Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81fa5f3a08 | |||
| cf0e9ad2bc | |||
| 07df19b412 | |||
| b9542e3980 | |||
| 4f5b6be884 | |||
| b8e8c01da5 | |||
| 62004360e8 | |||
| f616065585 |
@@ -1,12 +1,10 @@
|
||||
# SmartAssistant - עוזר חכם
|
||||
|
||||
**גרסה:** 1.0.3 | **מחבר:** klear | **EspoCRM:** >= 8.0.0
|
||||
**גרסה:** 1.1.7 | **מחבר:** klear | **EspoCRM:** >= 8.0.0
|
||||
|
||||
## תיאור
|
||||
עוזר AI מאוחד למשרד עורכי דין. מספק ממשק צ'אט צף עם שני מצבי עבודה: **מצב משרד** (סקירה כללית, התראות, סטטיסטיקות) ו**מצב תיק** (סיוע מעמיק בתיק ספציפי). כולל מערכת זיכרון תיק מובנית, ביצוע פעולות באישור המשתמש, ואינטגרציה עם Stream של EspoCRM.
|
||||
|
||||
מחליף את CrmAssistant + OfficeAssistant (deprecated).
|
||||
|
||||
## תלויות
|
||||
- Webhook חיצוני (n8n או דומה) לעיבוד AI
|
||||
- NetworkStorageIntegration / NextCloudIntegration (אופציונלי — לניתוח מסמכים)
|
||||
|
||||
@@ -5,7 +5,7 @@ define('modules/smart-assistant/views/dashlets/smart-assistant', ['view'], funct
|
||||
name: 'SmartAssistant',
|
||||
|
||||
getTitle: function () {
|
||||
return this.translate('Smart Assistant', 'labels', 'SmartAssistant') || 'Smart Assistant';
|
||||
return 'שירה — עוזרת אישית';
|
||||
},
|
||||
|
||||
templateContent: '' +
|
||||
@@ -45,6 +45,8 @@ define('modules/smart-assistant/views/dashlets/smart-assistant', ['view'], funct
|
||||
Espo.Ajax.getRequest('SmartAssistant/action/summary').then(function (response) {
|
||||
self._summary = response.summary || {};
|
||||
self._alerts = response.alerts || [];
|
||||
self._openCases = response.openCases || [];
|
||||
self._upcomingHearings = response.upcomingHearings || [];
|
||||
self.renderCards(self._summary);
|
||||
self.showDetail(self._activeType);
|
||||
}).catch(function () {});
|
||||
@@ -74,6 +76,7 @@ define('modules/smart-assistant/views/dashlets/smart-assistant', ['view'], funct
|
||||
},
|
||||
|
||||
showDetail: function (type) {
|
||||
var self = this;
|
||||
var $detail = this.$el.find('.sa-dashlet-detail');
|
||||
|
||||
if (!type || type === this._activeType && $detail.html()) {
|
||||
@@ -93,11 +96,11 @@ define('modules/smart-assistant/views/dashlets/smart-assistant', ['view'], funct
|
||||
if (type === 'overdue') {
|
||||
items = alerts.filter(function (a) { return a.type === 'overdue_task'; });
|
||||
} else if (type === 'hearings') {
|
||||
items = alerts.filter(function (a) { return a.type === 'hearing_no_prep'; });
|
||||
items = this._upcomingHearings || [];
|
||||
} else if (type === 'attention') {
|
||||
items = alerts.filter(function (a) { return a.severity === 'critical' || a.severity === 'warning'; });
|
||||
} 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) {
|
||||
@@ -117,7 +120,7 @@ define('modules/smart-assistant/views/dashlets/smart-assistant', ['view'], funct
|
||||
|
||||
html += '<div style="padding: 6px 8px; border-bottom: 1px solid #f0f0f0; font-size: 12px; color: ' + color + ';">' +
|
||||
'<span class="fas ' + icon + '" style="margin-left: 6px;"></span>' +
|
||||
link + Espo.Utils.escapeString(a.message) + linkEnd +
|
||||
link + self.escapeString(a.message) + linkEnd +
|
||||
'</div>';
|
||||
});
|
||||
|
||||
|
||||
@@ -480,6 +480,10 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
||||
var cls = approved ? (result.success ? 'sa-msg-assistant' : 'sa-msg-error') : 'sa-msg-loading';
|
||||
$messages.append('<div class="sa-msg ' + cls + '">' + self.escapeHtml(msg) + '</div>');
|
||||
self.scrollToBottom();
|
||||
|
||||
if (approved && result.success) {
|
||||
self.refreshCurrentView(result.entityType);
|
||||
}
|
||||
}).catch(function () {
|
||||
$messages.append('<div class="sa-msg sa-msg-error">שגיאה בביצוע הפעולה</div>');
|
||||
self.scrollToBottom();
|
||||
@@ -620,5 +624,37 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
},
|
||||
|
||||
refreshCurrentView: function (entityType) {
|
||||
var mainView = this.getParentView();
|
||||
|
||||
while (mainView && mainView.getParentView()) {
|
||||
mainView = mainView.getParentView();
|
||||
}
|
||||
|
||||
if (!mainView) return;
|
||||
|
||||
var currentView = mainView.getView('main');
|
||||
|
||||
if (!currentView) return;
|
||||
|
||||
var recordView = currentView.getView('record');
|
||||
|
||||
if (recordView && recordView.model) {
|
||||
recordView.model.trigger('update-all');
|
||||
}
|
||||
|
||||
if (entityType === 'Task' || entityType === 'Meeting') {
|
||||
var sideView = currentView.getView('side');
|
||||
|
||||
if (sideView) {
|
||||
var activities = sideView.getView('activities');
|
||||
var tasks = sideView.getView('tasks');
|
||||
|
||||
if (activities && activities.actionRefresh) activities.actionRefresh();
|
||||
if (tasks && tasks.actionRefresh) tasks.actionRefresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ define('modules/smart-assistant/views/stream/notes/smart-action', ['views/stream
|
||||
templateContent: '' +
|
||||
'<div class="sa-action-note sa-action-{{status}}" style="padding: 6px 12px; border-radius: 8px; direction: rtl; text-align: right; font-size: 13px;">' +
|
||||
'<span class="fas {{icon}}" style="margin-left: 6px;"></span>' +
|
||||
'{{post}}' +
|
||||
'{{{formattedPost}}}' +
|
||||
'</div>',
|
||||
|
||||
data: function () {
|
||||
@@ -13,6 +13,8 @@ define('modules/smart-assistant/views/stream/notes/smart-action', ['views/stream
|
||||
var noteData = this.model.get('data') || {};
|
||||
var status = noteData.status || 'executed';
|
||||
|
||||
data.post = this.model.get('post');
|
||||
data.formattedPost = this.getHelper().escapeString(data.post || '');
|
||||
data.status = status;
|
||||
|
||||
if (status === 'executed') {
|
||||
|
||||
+14
-3
@@ -5,11 +5,22 @@ define('modules/smart-assistant/views/stream/notes/smart-request', ['views/strea
|
||||
templateContent: '' +
|
||||
'<div style="background: #e3f2fd; padding: 8px 12px; border-radius: 8px; direction: rtl; text-align: right; font-size: 13px;">' +
|
||||
'<span class="fas fa-user" style="color: #1565c0; margin-left: 6px;"></span>' +
|
||||
'{{post}}' +
|
||||
'{{{formattedPost}}}' +
|
||||
'</div>',
|
||||
|
||||
setup: function () {
|
||||
NoteView.prototype.setup.call(this);
|
||||
data: function () {
|
||||
var data = NoteView.prototype.data.call(this);
|
||||
|
||||
data.formattedPost = this.formatPost(this.model.get('post'));
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
formatPost: function (text) {
|
||||
if (!text) return '';
|
||||
text = this.getHelper().escapeString(text);
|
||||
text = text.replace(/\n/g, '<br>');
|
||||
return text;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"itemViews": {
|
||||
"SmartRequest": "modules/smart-assistant/views/stream/notes/smart-request",
|
||||
"SmartResponse": "modules/smart-assistant/views/stream/notes/smart-response",
|
||||
"SmartAction": "modules/smart-assistant/views/stream/notes/smart-action"
|
||||
}
|
||||
}
|
||||
@@ -243,6 +243,78 @@ class AlertCalculator
|
||||
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');
|
||||
|
||||
@@ -55,8 +55,7 @@ class ConversationRepository
|
||||
if (!$entity) return;
|
||||
}
|
||||
|
||||
$messages = $entity->get('messagesData') ?? [];
|
||||
if (is_object($messages)) $messages = (array) $messages;
|
||||
$messages = json_decode(json_encode($entity->get('messagesData') ?? []), true);
|
||||
|
||||
$newMessage = ['role' => $role, 'content' => $content, 'timestamp' => date('Y-m-d H:i:s')];
|
||||
if (!empty($actions)) $newMessage['actions'] = $actions;
|
||||
@@ -81,8 +80,7 @@ class ConversationRepository
|
||||
$entity = $this->entityManager->getRDBRepository('AssistantConversation')
|
||||
->where(['conversationKey' => $conversationKey, 'deleted' => false])->findOne();
|
||||
if (!$entity) return [];
|
||||
$messages = $entity->get('messagesData') ?? [];
|
||||
return is_object($messages) ? (array) $messages : $messages;
|
||||
return json_decode(json_encode($entity->get('messagesData') ?? []), true);
|
||||
}
|
||||
|
||||
public function getRecentConversations(?string $type = null, ?string $caseId = null, int $limit = 20): array
|
||||
@@ -137,8 +135,7 @@ class ConversationRepository
|
||||
->where(['conversationKey' => $conversationKey, 'deleted' => false])->findOne();
|
||||
if (!$entity) return;
|
||||
|
||||
$data = $entity->get('messagesData') ?? [];
|
||||
if (is_object($data)) $data = (array) $data;
|
||||
$data = json_decode(json_encode($entity->get('messagesData') ?? []), true);
|
||||
for ($i = count($data) - 1; $i >= 0; $i--) {
|
||||
if (($data[$i]['role'] ?? '') === 'assistant') { $data[$i]['pendingActions'] = $actions; break; }
|
||||
}
|
||||
|
||||
@@ -250,10 +250,13 @@ class SmartAssistantService
|
||||
{
|
||||
$alertCalc = $this->injectableFactory->create(AlertCalculator::class);
|
||||
$thresholds = $this->getThresholds();
|
||||
$hearingDays = $thresholds['upcomingHearingDays'] ?? 7;
|
||||
|
||||
return [
|
||||
'summary' => $alertCalc->getSummary(),
|
||||
'alerts' => $alertCalc->calculateAlerts($thresholds),
|
||||
'openCases' => $alertCalc->getOpenCaseItems(),
|
||||
'upcomingHearings' => $alertCalc->getUpcomingHearingItems($hearingDays),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -416,14 +419,11 @@ class SmartAssistantService
|
||||
|
||||
private function getWebhookUrl(): ?string
|
||||
{
|
||||
// Try SmartAssistant first, then fallback to old integrations
|
||||
foreach (['SmartAssistant', 'CrmAssistant', 'OfficeAssistant'] as $name) {
|
||||
$integration = $this->entityManager->getEntityById('Integration', $name);
|
||||
if ($integration && $integration->get('enabled')) {
|
||||
$data = $integration->get('data') ?? (object) [];
|
||||
if (!empty($data->webhookUrl)) {
|
||||
return $data->webhookUrl;
|
||||
}
|
||||
$integration = $this->entityManager->getEntityById('Integration', 'SmartAssistant');
|
||||
if ($integration && $integration->get('enabled')) {
|
||||
$data = $integration->get('data') ?? (object) [];
|
||||
if (!empty($data->webhookUrl)) {
|
||||
return $data->webhookUrl;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -431,13 +431,11 @@ class SmartAssistantService
|
||||
|
||||
private function getApiKey(): ?string
|
||||
{
|
||||
foreach (['SmartAssistant', 'CrmAssistant', 'OfficeAssistant'] as $name) {
|
||||
$integration = $this->entityManager->getEntityById('Integration', $name);
|
||||
if ($integration && $integration->get('enabled')) {
|
||||
$data = $integration->get('data') ?? (object) [];
|
||||
if (!empty($data->apiKey)) {
|
||||
return $data->apiKey;
|
||||
}
|
||||
$integration = $this->entityManager->getEntityById('Integration', 'SmartAssistant');
|
||||
if ($integration && $integration->get('enabled')) {
|
||||
$data = $integration->get('data') ?? (object) [];
|
||||
if (!empty($data->apiKey)) {
|
||||
return $data->apiKey;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -445,16 +443,14 @@ class SmartAssistantService
|
||||
|
||||
private function getThresholds(): array
|
||||
{
|
||||
foreach (['SmartAssistant', 'OfficeAssistant'] as $name) {
|
||||
$integration = $this->entityManager->getEntityById('Integration', $name);
|
||||
if ($integration && $integration->get('enabled')) {
|
||||
$data = (array) ($integration->get('data') ?? []);
|
||||
return [
|
||||
'inactivityWarningDays' => $data['inactivityWarningDays'] ?? 14,
|
||||
'inactivityCriticalDays' => $data['inactivityCriticalDays'] ?? 30,
|
||||
'upcomingHearingDays' => $data['upcomingHearingDays'] ?? 7,
|
||||
];
|
||||
}
|
||||
$integration = $this->entityManager->getEntityById('Integration', 'SmartAssistant');
|
||||
if ($integration && $integration->get('enabled')) {
|
||||
$data = (array) ($integration->get('data') ?? []);
|
||||
return [
|
||||
'inactivityWarningDays' => $data['inactivityWarningDays'] ?? 14,
|
||||
'inactivityCriticalDays' => $data['inactivityCriticalDays'] ?? 30,
|
||||
'upcomingHearingDays' => $data['upcomingHearingDays'] ?? 7,
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "Smart Assistant",
|
||||
"name": "SmartAssistant",
|
||||
"module": "SmartAssistant",
|
||||
"description": "Unified AI Assistant for Legal CRM — floating chat with case memory, office alerts, and n8n integration",
|
||||
"author": "klear",
|
||||
"version": "1.0.9",
|
||||
"version": "1.1.7",
|
||||
"acceptableVersions": [
|
||||
">=8.0.0"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user