Compare commits

...

1 Commits

Author SHA1 Message Date
chaim f616065585 fix: dashlet detail shows actual items instead of empty alerts
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>
2026-04-03 10:25:31 +00:00
5 changed files with 101 additions and 33 deletions
+1 -3
View File
@@ -1,12 +1,10 @@
# SmartAssistant - עוזר חכם # SmartAssistant - עוזר חכם
**גרסה:** 1.0.3 | **מחבר:** klear | **EspoCRM:** >= 8.0.0 **גרסה:** 1.1.0 | **מחבר:** klear | **EspoCRM:** >= 8.0.0
## תיאור ## תיאור
עוזר AI מאוחד למשרד עורכי דין. מספק ממשק צ'אט צף עם שני מצבי עבודה: **מצב משרד** (סקירה כללית, התראות, סטטיסטיקות) ו**מצב תיק** (סיוע מעמיק בתיק ספציפי). כולל מערכת זיכרון תיק מובנית, ביצוע פעולות באישור המשתמש, ואינטגרציה עם Stream של EspoCRM. עוזר AI מאוחד למשרד עורכי דין. מספק ממשק צ'אט צף עם שני מצבי עבודה: **מצב משרד** (סקירה כללית, התראות, סטטיסטיקות) ו**מצב תיק** (סיוע מעמיק בתיק ספציפי). כולל מערכת זיכרון תיק מובנית, ביצוע פעולות באישור המשתמש, ואינטגרציה עם Stream של EspoCRM.
מחליף את CrmAssistant + OfficeAssistant (deprecated).
## תלויות ## תלויות
- Webhook חיצוני (n8n או דומה) לעיבוד AI - Webhook חיצוני (n8n או דומה) לעיבוד AI
- NetworkStorageIntegration / NextCloudIntegration (אופציונלי — לניתוח מסמכים) - NetworkStorageIntegration / NextCloudIntegration (אופציונלי — לניתוח מסמכים)
@@ -5,7 +5,7 @@ define('modules/smart-assistant/views/dashlets/smart-assistant', ['view'], funct
name: 'SmartAssistant', name: 'SmartAssistant',
getTitle: function () { getTitle: function () {
return this.translate('Smart Assistant', 'labels', 'SmartAssistant') || 'Smart Assistant'; return 'שירה — עוזרת אישית';
}, },
templateContent: '' + templateContent: '' +
@@ -45,6 +45,8 @@ define('modules/smart-assistant/views/dashlets/smart-assistant', ['view'], funct
Espo.Ajax.getRequest('SmartAssistant/action/summary').then(function (response) { Espo.Ajax.getRequest('SmartAssistant/action/summary').then(function (response) {
self._summary = response.summary || {}; self._summary = response.summary || {};
self._alerts = response.alerts || []; self._alerts = response.alerts || [];
self._openCases = response.openCases || [];
self._upcomingHearings = response.upcomingHearings || [];
self.renderCards(self._summary); self.renderCards(self._summary);
self.showDetail(self._activeType); self.showDetail(self._activeType);
}).catch(function () {}); }).catch(function () {});
@@ -93,11 +95,11 @@ define('modules/smart-assistant/views/dashlets/smart-assistant', ['view'], funct
if (type === 'overdue') { if (type === 'overdue') {
items = alerts.filter(function (a) { return a.type === 'overdue_task'; }); items = alerts.filter(function (a) { return a.type === 'overdue_task'; });
} else if (type === 'hearings') { } else if (type === 'hearings') {
items = alerts.filter(function (a) { return a.type === 'hearing_no_prep'; }); items = this._upcomingHearings || [];
} else if (type === 'attention') { } else if (type === 'attention') {
items = alerts.filter(function (a) { return a.severity === 'critical' || a.severity === 'warning'; }); items = alerts.filter(function (a) { return a.severity === 'critical' || a.severity === 'warning'; });
} else if (type === 'open') { } else if (type === 'open') {
items = alerts.filter(function (a) { return a.type === 'inactive_case' || a.type === 'unassigned_case'; }); items = this._openCases || [];
} }
if (items.length === 0) { if (items.length === 0) {
@@ -243,6 +243,78 @@ class AlertCalculator
return array_values($userCounts); 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 public function getUpcomingHearings(int $days = 7): array
{ {
$today = date('Y-m-d'); $today = date('Y-m-d');
@@ -250,10 +250,13 @@ class SmartAssistantService
{ {
$alertCalc = $this->injectableFactory->create(AlertCalculator::class); $alertCalc = $this->injectableFactory->create(AlertCalculator::class);
$thresholds = $this->getThresholds(); $thresholds = $this->getThresholds();
$hearingDays = $thresholds['upcomingHearingDays'] ?? 7;
return [ return [
'summary' => $alertCalc->getSummary(), 'summary' => $alertCalc->getSummary(),
'alerts' => $alertCalc->calculateAlerts($thresholds), 'alerts' => $alertCalc->calculateAlerts($thresholds),
'openCases' => $alertCalc->getOpenCaseItems(),
'upcomingHearings' => $alertCalc->getUpcomingHearingItems($hearingDays),
]; ];
} }
@@ -416,14 +419,11 @@ class SmartAssistantService
private function getWebhookUrl(): ?string private function getWebhookUrl(): ?string
{ {
// Try SmartAssistant first, then fallback to old integrations $integration = $this->entityManager->getEntityById('Integration', 'SmartAssistant');
foreach (['SmartAssistant', 'CrmAssistant', 'OfficeAssistant'] as $name) { if ($integration && $integration->get('enabled')) {
$integration = $this->entityManager->getEntityById('Integration', $name); $data = $integration->get('data') ?? (object) [];
if ($integration && $integration->get('enabled')) { if (!empty($data->webhookUrl)) {
$data = $integration->get('data') ?? (object) []; return $data->webhookUrl;
if (!empty($data->webhookUrl)) {
return $data->webhookUrl;
}
} }
} }
return null; return null;
@@ -431,13 +431,11 @@ class SmartAssistantService
private function getApiKey(): ?string private function getApiKey(): ?string
{ {
foreach (['SmartAssistant', 'CrmAssistant', 'OfficeAssistant'] as $name) { $integration = $this->entityManager->getEntityById('Integration', 'SmartAssistant');
$integration = $this->entityManager->getEntityById('Integration', $name); if ($integration && $integration->get('enabled')) {
if ($integration && $integration->get('enabled')) { $data = $integration->get('data') ?? (object) [];
$data = $integration->get('data') ?? (object) []; if (!empty($data->apiKey)) {
if (!empty($data->apiKey)) { return $data->apiKey;
return $data->apiKey;
}
} }
} }
return null; return null;
@@ -445,16 +443,14 @@ class SmartAssistantService
private function getThresholds(): array private function getThresholds(): array
{ {
foreach (['SmartAssistant', 'OfficeAssistant'] as $name) { $integration = $this->entityManager->getEntityById('Integration', 'SmartAssistant');
$integration = $this->entityManager->getEntityById('Integration', $name); if ($integration && $integration->get('enabled')) {
if ($integration && $integration->get('enabled')) { $data = (array) ($integration->get('data') ?? []);
$data = (array) ($integration->get('data') ?? []); return [
return [ 'inactivityWarningDays' => $data['inactivityWarningDays'] ?? 14,
'inactivityWarningDays' => $data['inactivityWarningDays'] ?? 14, 'inactivityCriticalDays' => $data['inactivityCriticalDays'] ?? 30,
'inactivityCriticalDays' => $data['inactivityCriticalDays'] ?? 30, 'upcomingHearingDays' => $data['upcomingHearingDays'] ?? 7,
'upcomingHearingDays' => $data['upcomingHearingDays'] ?? 7, ];
];
}
} }
return []; return [];
} }
+2 -2
View File
@@ -1,9 +1,9 @@
{ {
"name": "Smart Assistant", "name": "SmartAssistant",
"module": "SmartAssistant", "module": "SmartAssistant",
"description": "Unified AI Assistant for Legal CRM — floating chat with case memory, office alerts, and n8n integration", "description": "Unified AI Assistant for Legal CRM — floating chat with case memory, office alerts, and n8n integration",
"author": "klear", "author": "klear",
"version": "1.0.9", "version": "1.1.0",
"acceptableVersions": [ "acceptableVersions": [
">=8.0.0" ">=8.0.0"
], ],