Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 07df19b412 | |||
| b9542e3980 | |||
| 4f5b6be884 | |||
| b8e8c01da5 | |||
| 62004360e8 | |||
| f616065585 | |||
| 1eb856b178 | |||
| c9d08aa1b9 | |||
| 233f56eccf | |||
| 0157b31249 | |||
| 49c42e3d20 | |||
| 4dad65f578 | |||
| f116611c73 | |||
| ff2388e7e1 | |||
| 92c3cd2cdc | |||
| 3102bcff77 | |||
| f5c60f4272 | |||
| 21c3e33635 | |||
| ffbb648dd5 | |||
| f915faafdc | |||
| b9abe5f921 | |||
| a3801deb55 | |||
| 300cc58a97 | |||
| c1ea2b7868 | |||
| 184cd19285 | |||
| 2c34d56c64 | |||
| d5c8a60193 | |||
| 96e7c8e9e0 | |||
| f3805424d3 | |||
| 1eea85da2b | |||
| fbe4f82517 | |||
| 95ca8a0af4 | |||
| bd1cd17490 | |||
| ffd966a4f0 |
@@ -0,0 +1,120 @@
|
||||
# SmartAssistant - עוזר חכם
|
||||
|
||||
**גרסה:** 1.1.5 | **מחבר:** klear | **EspoCRM:** >= 8.0.0
|
||||
|
||||
## תיאור
|
||||
עוזר AI מאוחד למשרד עורכי דין. מספק ממשק צ'אט צף עם שני מצבי עבודה: **מצב משרד** (סקירה כללית, התראות, סטטיסטיקות) ו**מצב תיק** (סיוע מעמיק בתיק ספציפי). כולל מערכת זיכרון תיק מובנית, ביצוע פעולות באישור המשתמש, ואינטגרציה עם Stream של EspoCRM.
|
||||
|
||||
## תלויות
|
||||
- Webhook חיצוני (n8n או דומה) לעיבוד AI
|
||||
- NetworkStorageIntegration / NextCloudIntegration (אופציונלי — לניתוח מסמכים)
|
||||
|
||||
## התקנה
|
||||
1. התקנה דרך Admin > Extensions
|
||||
2. הגדרת Webhook: Admin > Integrations > Smart Assistant
|
||||
|
||||
## הגדרות אינטגרציה
|
||||
|
||||
| שדה | תיאור | דוגמה |
|
||||
|------|--------|--------|
|
||||
| webhookUrl | כתובת Webhook לשירות AI | `https://n8n.example.com/webhook/assistant/chat` |
|
||||
| apiKey | מפתח אימות (אופציונלי) | `sk-...` |
|
||||
| maxMessagesPerHour | הגבלת קצב הודעות | `30` |
|
||||
| inactivityWarningDays | סף אזהרה לחוסר פעילות בתיק | `14` |
|
||||
| inactivityCriticalDays | סף קריטי לחוסר פעילות | `30` |
|
||||
| upcomingHearingDays | חלון דיונים קרובים (ימים) | `7` |
|
||||
|
||||
## אנטיטי: CaseMemory — זיכרון תיק
|
||||
|
||||
מאגר ידע מובנה לכל תיק, מחולק לקטגוריות:
|
||||
|
||||
| קטגוריה | תיאור |
|
||||
|----------|--------|
|
||||
| key_facts | עובדות מפתח |
|
||||
| strategy | אסטרטגיה |
|
||||
| decisions | החלטות |
|
||||
| contacts_notes | הערות על אנשי קשר |
|
||||
| timeline | ציר זמן |
|
||||
| documents_notes | הערות על מסמכים |
|
||||
| billing_notes | הערות חיוב |
|
||||
|
||||
**מקורות:** ידני (manual), עוזר AI (assistant), אוטומטי (auto — hooks)
|
||||
|
||||
**רמות חשיבות:** low, normal, high, critical
|
||||
|
||||
## רכיבים
|
||||
|
||||
### Backend (PHP)
|
||||
|
||||
| רכיב | קובץ | תיאור |
|
||||
|-------|------|--------|
|
||||
| Controller | `Controllers/SmartAssistant.php` | API endpoints |
|
||||
| שירות ראשי | `Services/SmartAssistantService.php` | תזמור צ'אט, שיחות, פעולות |
|
||||
| בונה הקשר תיק | `Services/CaseContextBuilder.php` | אוסף נתוני תיק מלאים (אנשי קשר, משימות, מסמכים, זיכרון) |
|
||||
| בונה הקשר משרד | `Services/OfficeContextBuilder.php` | סטטיסטיקות, התראות, עומס עבודה |
|
||||
| מחשבון התראות | `Services/AlertCalculator.php` | זיהוי תיקים לא פעילים, משימות באיחור, דיונים קרובים |
|
||||
| מבצע פעולות | `Services/ActionExecutor.php` | ביצוע פעולות שאושרו (משימה, פתק, דיון, שינוי סטטוס) |
|
||||
| שירות זיכרון | `Services/CaseMemoryService.php` | CRUD לזיכרון תיק |
|
||||
| ספק הקשר זיכרון | `Services/CaseMemoryContextProvider.php` | הזרקת זיכרון להקשר AI |
|
||||
| מאגר שיחות | `Services/ConversationRepository.php` | שמירה ואחזור שיחות |
|
||||
| מנתח מסמכים | `Services/DocumentAnalyzer.php` | סריקת מסמכים וסיווג |
|
||||
|
||||
### Hooks
|
||||
|
||||
| Hook | תיאור |
|
||||
|------|--------|
|
||||
| `Case/CaseFieldChangeMemory` | יצירת זיכרון אוטומטית בשינוי שדות תיק (סטטוס, שופט, דיון) |
|
||||
| `Meeting/HearingScheduledMemory` | יצירת זיכרון בקביעת דיון חדש |
|
||||
|
||||
### Frontend (JavaScript)
|
||||
|
||||
| רכיב | תיאור |
|
||||
|-------|--------|
|
||||
| `floating-chat.js` | ממשק צ'אט צף (FAB) עם מגירת היסטוריה וזיכרון |
|
||||
| `dashlets/smart-assistant.js` | דשלט עם מדדים והתראות |
|
||||
| `stream/notes/smart-request.js` | תצוגת הודעת משתמש ב-Stream |
|
||||
| `stream/notes/smart-response.js` | תצוגת תשובת עוזר ב-Stream |
|
||||
| `stream/notes/smart-action.js` | תצוגת פעולה שבוצעה ב-Stream |
|
||||
| `case/modals/add-memory.js` | מודל להוספת זיכרון ידנית |
|
||||
| `site/navbar.js` | אינטגרציית התראות בסרגל ניווט |
|
||||
|
||||
## API Routes
|
||||
|
||||
| נתיב | Method | תיאור |
|
||||
|-------|--------|--------|
|
||||
| `/SmartAssistant/action/chat` | POST | שליחת הודעה לעוזר |
|
||||
| `/SmartAssistant/action/execute` | POST | ביצוע פעולה שאושרה |
|
||||
| `/SmartAssistant/action/summary` | GET | סיכום משרדי + התראות |
|
||||
| `/SmartAssistant/action/alerts` | GET | רשימת התראות |
|
||||
| `/SmartAssistant/action/history` | GET | היסטוריית שיחות |
|
||||
| `/SmartAssistant/action/conversations` | GET | רשימת שיחות אחרונות |
|
||||
| `/SmartAssistant/action/conversationMessages` | GET | הודעות שיחה ספציפית |
|
||||
| `/SmartAssistant/action/searchHistory` | GET | חיפוש בשיחות |
|
||||
| `/SmartAssistant/action/caseMemory` | GET | אחזור זיכרון תיק |
|
||||
| `/SmartAssistant/action/saveMemory` | POST | שמירת זיכרון ידנית |
|
||||
|
||||
## פעולות עוזר
|
||||
|
||||
### פעולות מיידיות (ללא אישור)
|
||||
- `list_documents` — רשימת מסמכים בתיק
|
||||
- `save_memory` — שמירת זיכרון תיק
|
||||
|
||||
### פעולות הדורשות אישור
|
||||
- `create_task` — יצירת משימה
|
||||
- `add_note` — הוספת הערה
|
||||
- `change_status` — שינוי סטטוס תיק
|
||||
- `create_meeting` — יצירת פגישה
|
||||
- `schedule_hearing` — קביעת דיון
|
||||
- `analyze_document` — ניתוח מסמך
|
||||
- `rename_document` — שינוי שם מסמך
|
||||
|
||||
## מערכת התראות
|
||||
|
||||
| סוג | סף ברירת מחדל | חומרה |
|
||||
|------|----------------|--------|
|
||||
| תיק לא פעיל | 14 יום | אזהרה |
|
||||
| תיק לא פעיל | 30 יום | קריטי |
|
||||
| משימה באיחור | תאריך יעד עבר | קריטי |
|
||||
| דיון קרוב | 7 ימים | אזהרה |
|
||||
| תיק ללא שיוך | חדש ללא assignedUser | אזהרה |
|
||||
| משימה בעדיפות גבוהה | 5 ימים | אזהרה |
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,6 +4,11 @@ set -euo pipefail
|
||||
VERSION=$(python3 -c "import json; print(json.load(open('manifest.json'))['version'])")
|
||||
MODULE=$(python3 -c "import json; m=json.load(open('manifest.json')); print(m.get('module', m['name']))")
|
||||
ZIPNAME="${MODULE}-${VERSION}.zip"
|
||||
|
||||
# Update version in README.md if it exists
|
||||
if [[ -f README.md ]]; then
|
||||
sed -i "s/\*\*גרסה:\*\* [^ |]*/\*\*גרסה:\*\* ${VERSION}/" README.md
|
||||
fi
|
||||
echo "Building $ZIPNAME..."
|
||||
rm -f "$ZIPNAME"
|
||||
zip -r "$ZIPNAME" manifest.json files/ \
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "espocrm-extensions/smart-assistant",
|
||||
"description": "Unified AI Assistant for Legal CRM — floating chat with case memory, office alerts, and n8n integration",
|
||||
"type": "espocrm-extension",
|
||||
"license": "proprietary",
|
||||
"authors": [{"name": "klear"}],
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"extra": {
|
||||
"espocrm-module": "SmartAssistant",
|
||||
"espocrm-min-version": ">=8.0.0"
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
define('custom:modules/smart-assistant/views/case/modals/add-memory', ['views/modal'], function (ModalView) {
|
||||
define('modules/smart-assistant/views/case/modals/add-memory', ['views/modal'], function (ModalView) {
|
||||
|
||||
return ModalView.extend({
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
define('custom:modules/smart-assistant/views/dashlets/options/smart-assistant', ['views/dashlets/options/base'], function (BaseView) {
|
||||
define('modules/smart-assistant/views/dashlets/options/smart-assistant', ['views/dashlets/options/base'], function (BaseView) {
|
||||
|
||||
return BaseView.extend({
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
define('modules/smart-assistant/views/dashlets/smart-assistant', ['view'], function (View) {
|
||||
|
||||
return View.extend({
|
||||
|
||||
name: 'SmartAssistant',
|
||||
|
||||
getTitle: function () {
|
||||
return 'שירה — עוזרת אישית';
|
||||
},
|
||||
|
||||
templateContent: '' +
|
||||
'<div style="direction: rtl; text-align: right; padding: 8px;">' +
|
||||
'<div class="sa-dashlet-cards" style="display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px;"></div>' +
|
||||
'<div class="sa-dashlet-detail" style="max-height: 250px; overflow-y: auto;"></div>' +
|
||||
'</div>',
|
||||
|
||||
events: {
|
||||
'click .sa-card': function (e) {
|
||||
var type = $(e.currentTarget).data('type');
|
||||
this.showDetail(type);
|
||||
},
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
this._alerts = [];
|
||||
this._summary = {};
|
||||
this._activeType = null;
|
||||
|
||||
var self = this;
|
||||
Espo.Ajax.getRequest('SmartAssistant/action/status').then(function (result) {
|
||||
if (!result.enabled) {
|
||||
self.$el.find('.sa-dashlet-cards').html(
|
||||
'<div style="text-align: center; color: #999; padding: 20px; font-size: 13px;">שירה לא מופעלת. הפעל ב-Admin > Integrations > Smart Assistant</div>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
self.loadSummary();
|
||||
}).catch(function () {
|
||||
self.loadSummary();
|
||||
});
|
||||
},
|
||||
|
||||
loadSummary: function () {
|
||||
var self = this;
|
||||
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 () {});
|
||||
},
|
||||
|
||||
renderCards: function (summary) {
|
||||
var cards = [
|
||||
{type: 'open', label: 'תיקים פתוחים', value: summary.totalOpenCases || 0, color: '#1565c0', bg: '#e3f2fd', icon: 'fa-folder-open'},
|
||||
{type: 'overdue', label: 'משימות באיחור', value: summary.totalOverdueTasks || 0, color: '#c62828', bg: '#ffcdd2', icon: 'fa-exclamation-circle'},
|
||||
{type: 'hearings', label: 'דיונים השבוע', value: summary.upcomingHearings7d || 0, color: '#e65100', bg: '#fff3e0', icon: 'fa-gavel'},
|
||||
{type: 'attention', label: 'דורשים תשומת לב', value: summary.casesNeedingAttention || 0, color: '#f9a825', bg: '#fff9c4', icon: 'fa-bell'},
|
||||
];
|
||||
|
||||
var self = this;
|
||||
var html = '';
|
||||
cards.forEach(function (c) {
|
||||
var active = self._activeType === c.type;
|
||||
var border = active ? '2px solid ' + c.color : '2px solid transparent';
|
||||
var cursor = c.value > 0 ? 'pointer' : 'default';
|
||||
html += '<div class="sa-card" data-type="' + c.type + '" style="flex: 1; min-width: 80px; background: ' + c.bg + '; padding: 10px; border-radius: 8px; text-align: center; cursor: ' + cursor + '; border: ' + border + '; transition: border 0.2s;">' +
|
||||
'<div style="font-size: 22px; font-weight: bold; color: ' + c.color + ';">' + c.value + '</div>' +
|
||||
'<div style="font-size: 11px; color: ' + c.color + ';"><span class="fas ' + c.icon + '"></span> ' + c.label + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
|
||||
this.$el.find('.sa-dashlet-cards').html(html);
|
||||
},
|
||||
|
||||
showDetail: function (type) {
|
||||
var self = this;
|
||||
var $detail = this.$el.find('.sa-dashlet-detail');
|
||||
|
||||
if (!type || type === this._activeType && $detail.html()) {
|
||||
// Toggle off
|
||||
this._activeType = null;
|
||||
$detail.html('');
|
||||
this.renderCards(this._summary);
|
||||
return;
|
||||
}
|
||||
|
||||
this._activeType = type;
|
||||
this.renderCards(this._summary);
|
||||
|
||||
var alerts = this._alerts;
|
||||
var items = [];
|
||||
|
||||
if (type === 'overdue') {
|
||||
items = alerts.filter(function (a) { return a.type === 'overdue_task'; });
|
||||
} else if (type === 'hearings') {
|
||||
items = this._upcomingHearings || [];
|
||||
} else if (type === 'attention') {
|
||||
items = alerts.filter(function (a) { return a.severity === 'critical' || a.severity === 'warning'; });
|
||||
} else if (type === 'open') {
|
||||
items = this._openCases || [];
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
$detail.html('<div style="text-align: center; color: #999; padding: 12px; font-size: 12px;">אין פריטים להצגה</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
var severityColors = {critical: '#c62828', warning: '#e65100', info: '#1565c0'};
|
||||
var severityIcons = {critical: 'fa-exclamation-circle', warning: 'fa-exclamation-triangle', info: 'fa-info-circle'};
|
||||
|
||||
var html = '';
|
||||
items.forEach(function (a) {
|
||||
var color = severityColors[a.severity] || '#333';
|
||||
var icon = severityIcons[a.severity] || 'fa-circle';
|
||||
var link = a.caseId ? '<a href="#Case/view/' + a.caseId + '" style="color: ' + color + '; text-decoration: none;">' : '<span>';
|
||||
var linkEnd = a.caseId ? '</a>' : '</span>';
|
||||
|
||||
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 + self.escapeString(a.message) + linkEnd +
|
||||
'</div>';
|
||||
});
|
||||
|
||||
$detail.html(html);
|
||||
},
|
||||
|
||||
actionRefresh: function () {
|
||||
this.loadSummary();
|
||||
},
|
||||
|
||||
getActionItemDataList: function () {
|
||||
return [
|
||||
{
|
||||
name: 'refresh',
|
||||
iconHtml: '<span class="fas fa-sync-alt"></span>',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
getColor: function () {
|
||||
return '#5c6bc0';
|
||||
},
|
||||
});
|
||||
});
|
||||
+18
-6
@@ -1,4 +1,4 @@
|
||||
define('custom:modules/smart-assistant/views/floating-chat', ['view'], function (View) {
|
||||
define('modules/smart-assistant/views/floating-chat', ['view'], function (View) {
|
||||
|
||||
return View.extend({
|
||||
|
||||
@@ -158,10 +158,22 @@ define('custom:modules/smart-assistant/views/floating-chat', ['view'], function
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
this.$el.find('.sa-chat-messages').attr('data-empty-text',
|
||||
this.translate('Chat with the assistant', 'labels', 'SmartAssistant') || 'שוחח/י עם העוזר החכם');
|
||||
this.loadAlertCount();
|
||||
this._setupRouteListener();
|
||||
var self = this;
|
||||
|
||||
Espo.Ajax.getRequest('SmartAssistant/action/status').then(function (result) {
|
||||
if (!result.enabled) {
|
||||
self.$el.find('.sa-fab').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
self.$el.find('.sa-fab').show();
|
||||
self.$el.find('.sa-chat-messages').attr('data-empty-text',
|
||||
self.translate('Chat with the assistant', 'labels', 'SmartAssistant') || 'שוחח/י עם העוזר החכם');
|
||||
self.loadAlertCount();
|
||||
self._setupRouteListener();
|
||||
}).catch(function () {
|
||||
self.$el.find('.sa-fab').hide();
|
||||
});
|
||||
},
|
||||
|
||||
_setupRouteListener: function () {
|
||||
@@ -562,7 +574,7 @@ define('custom:modules/smart-assistant/views/floating-chat', ['view'], function
|
||||
if (!this.currentCaseId) return;
|
||||
var self = this;
|
||||
|
||||
this.createView('addMemoryModal', 'custom:modules/smart-assistant/views/case/modals/add-memory', {
|
||||
this.createView('addMemoryModal', 'modules/smart-assistant/views/case/modals/add-memory', {
|
||||
caseId: this.currentCaseId,
|
||||
}, function (view) {
|
||||
view.render();
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
define('custom:modules/smart-assistant/views/site/navbar', ['views/site/navbar'], function (NavbarView) {
|
||||
define('modules/smart-assistant/views/site/navbar', ['views/site/navbar'], function (NavbarView) {
|
||||
|
||||
return NavbarView.extend({
|
||||
|
||||
@@ -17,7 +17,7 @@ define('custom:modules/smart-assistant/views/site/navbar', ['views/site/navbar']
|
||||
$('body').append($container);
|
||||
}
|
||||
|
||||
this.createView('floatingChat', 'custom:modules/smart-assistant/views/floating-chat', {
|
||||
this.createView('floatingChat', 'modules/smart-assistant/views/floating-chat', {
|
||||
el: '.smart-assistant-fab-container',
|
||||
}, function (view) {
|
||||
view.render();
|
||||
+4
-2
@@ -1,11 +1,11 @@
|
||||
define('custom:modules/smart-assistant/views/stream/notes/smart-action', ['views/stream/note'], function (NoteView) {
|
||||
define('modules/smart-assistant/views/stream/notes/smart-action', ['views/stream/note'], function (NoteView) {
|
||||
|
||||
return NoteView.extend({
|
||||
|
||||
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('custom:modules/smart-assistant/views/stream/notes/smart-action', ['views
|
||||
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') {
|
||||
@@ -0,0 +1,26 @@
|
||||
define('modules/smart-assistant/views/stream/notes/smart-request', ['views/stream/note'], function (NoteView) {
|
||||
|
||||
return NoteView.extend({
|
||||
|
||||
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>' +
|
||||
'{{{formattedPost}}}' +
|
||||
'</div>',
|
||||
|
||||
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;
|
||||
},
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
define('custom:modules/smart-assistant/views/stream/notes/smart-response', ['views/stream/note'], function (NoteView) {
|
||||
define('modules/smart-assistant/views/stream/notes/smart-response', ['views/stream/note'], function (NoteView) {
|
||||
|
||||
return NoteView.extend({
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
define('custom:modules/smart-assistant/views/dashlets/smart-assistant', ['view'], function (View) {
|
||||
|
||||
return View.extend({
|
||||
|
||||
name: 'SmartAssistant',
|
||||
|
||||
getTitle: function () {
|
||||
return this.translate('Smart Assistant', 'labels', 'SmartAssistant') || 'Smart Assistant';
|
||||
},
|
||||
|
||||
templateContent: '' +
|
||||
'<div style="direction: rtl; text-align: right; padding: 8px;">' +
|
||||
'<div class="sa-dashlet-cards" style="display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px;"></div>' +
|
||||
'<div class="sa-dashlet-alerts" style="max-height: 200px; overflow-y: auto;"></div>' +
|
||||
'</div>',
|
||||
|
||||
afterRender: function () {
|
||||
this.loadSummary();
|
||||
},
|
||||
|
||||
loadSummary: function () {
|
||||
var self = this;
|
||||
Espo.Ajax.getRequest('SmartAssistant/action/summary').then(function (response) {
|
||||
self.renderCards(response.summary || {});
|
||||
self.renderAlerts(response.alerts || []);
|
||||
}).catch(function () {});
|
||||
},
|
||||
|
||||
renderCards: function (summary) {
|
||||
var cards = [
|
||||
{label: 'תיקים פתוחים', value: summary.totalOpenCases || 0, color: '#1565c0', bg: '#e3f2fd', icon: 'fa-folder-open'},
|
||||
{label: 'משימות באיחור', value: summary.totalOverdueTasks || 0, color: '#c62828', bg: '#ffcdd2', icon: 'fa-exclamation-circle'},
|
||||
{label: 'דיונים השבוע', value: summary.upcomingHearings7d || 0, color: '#e65100', bg: '#fff3e0', icon: 'fa-gavel'},
|
||||
{label: 'דורשים תשומת לב', value: summary.casesNeedingAttention || 0, color: '#f9a825', bg: '#fff9c4', icon: 'fa-bell'},
|
||||
];
|
||||
|
||||
var html = '';
|
||||
cards.forEach(function (c) {
|
||||
html += '<div style="flex: 1; min-width: 80px; background: ' + c.bg + '; padding: 10px; border-radius: 8px; text-align: center;">' +
|
||||
'<div style="font-size: 22px; font-weight: bold; color: ' + c.color + ';">' + c.value + '</div>' +
|
||||
'<div style="font-size: 11px; color: ' + c.color + ';"><span class="fas ' + c.icon + '"></span> ' + c.label + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
|
||||
this.$el.find('.sa-dashlet-cards').html(html);
|
||||
},
|
||||
|
||||
renderAlerts: function (alerts) {
|
||||
var $alerts = this.$el.find('.sa-dashlet-alerts');
|
||||
if (alerts.length === 0) {
|
||||
$alerts.html('<div style="text-align: center; color: #4caf50; padding: 12px;">אין התראות</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
var severityColors = {critical: '#c62828', warning: '#e65100', info: '#1565c0'};
|
||||
var severityIcons = {critical: 'fa-exclamation-circle', warning: 'fa-exclamation-triangle', info: 'fa-info-circle'};
|
||||
|
||||
var html = '';
|
||||
alerts.slice(0, 10).forEach(function (a) {
|
||||
var color = severityColors[a.severity] || '#333';
|
||||
var icon = severityIcons[a.severity] || 'fa-circle';
|
||||
|
||||
html += '<div style="padding: 6px 0; border-bottom: 1px solid #f5f5f5; font-size: 12px; color: ' + color + ';">' +
|
||||
'<span class="fas ' + icon + '" style="margin-left: 6px;"></span>' +
|
||||
'<span>' + Espo.Utils.escapeString(a.message) + '</span>' +
|
||||
'</div>';
|
||||
});
|
||||
|
||||
$alerts.html(html);
|
||||
},
|
||||
|
||||
actionRefresh: function () {
|
||||
this.loadSummary();
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
define('custom:modules/smart-assistant/views/stream/notes/smart-request', ['views/stream/note'], function (NoteView) {
|
||||
|
||||
return NoteView.extend({
|
||||
|
||||
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}}' +
|
||||
'</div>',
|
||||
|
||||
setup: function () {
|
||||
NoteView.prototype.setup.call(this);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -34,6 +34,11 @@ class SmartAssistant
|
||||
}
|
||||
}
|
||||
|
||||
public function getActionStatus(Request $request, Response $response): array
|
||||
{
|
||||
return $this->getService()->getStatus();
|
||||
}
|
||||
|
||||
public function postActionChat(Request $request, Response $response): array
|
||||
{
|
||||
$this->checkAccess();
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"labels":{"Create AssistantConversation":"New Conversation"},"fields":{"name":"Title","conversationKey":"Conversation Key","conversationType":"Type","status":"Status","assignedUser":"User","case":"Case","caseName":"Case Name","messageCount":"Messages","messagesData":"Messages Data","lastMessageAt":"Last Message","lastMessagePreview":"Preview","createdAt":"Created At","modifiedAt":"Modified At","createdBy":"Created By","modifiedBy":"Modified By"},"links":{"assignedUser":"User","case":"Case"},"options":{"conversationType":{"office":"Office","case":"Case"}}}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"labels":{"Create AssistantConversation":"שיחה חדשה"},"fields":{"name":"כותרת","conversationKey":"מזהה שיחה","conversationType":"סוג","status":"סטטוס","assignedUser":"משתמש","case":"תיק","caseName":"שם התיק","messageCount":"הודעות","messagesData":"נתוני הודעות","lastMessageAt":"הודעה אחרונה","lastMessagePreview":"תצוגה מקדימה","createdAt":"נוצר ב","modifiedAt":"עודכן ב","createdBy":"נוצר על ידי","modifiedBy":"עודכן על ידי"},"links":{"assignedUser":"משתמש","case":"תיק"},"options":{"conversationType":{"office":"משרד","case":"תיק"}}}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"labels": {
|
||||
"Smart Assistant": "עוזר חכם",
|
||||
"Smart Assistant": "שירה",
|
||||
"Office Mode": "מצב משרד",
|
||||
"Case Mode": "מצב תיק",
|
||||
"Add Memory": "הוסף זיכרון",
|
||||
@@ -9,11 +9,11 @@
|
||||
"Conversation History": "היסטוריית שיחות",
|
||||
"Previous Conversations": "שיחות קודמות",
|
||||
"No previous conversations": "אין שיחות קודמות",
|
||||
"Ask the assistant...": "שאל/י את העוזר...",
|
||||
"Ask the assistant...": "שאל/י את שירה...",
|
||||
"Thinking": "חושב...",
|
||||
"Error": "שגיאה בתקשורת",
|
||||
"Back": "חזרה",
|
||||
"Chat with the assistant": "שוחח/י עם העוזר החכם",
|
||||
"Chat with the assistant": "שוחח/י עם שירה",
|
||||
"Case Memory": "זיכרון תיק",
|
||||
"Pin": "הצמד",
|
||||
"Unpin": "הסר הצמדה",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"navbarView": "custom:modules/smart-assistant/views/site/navbar"
|
||||
"navbarView": "modules/smart-assistant/views/site/navbar"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"view": "custom:modules/smart-assistant/views/dashlets/smart-assistant",
|
||||
"optionsView": "custom:modules/smart-assistant/views/dashlets/options/smart-assistant",
|
||||
"view": "modules/smart-assistant/views/dashlets/smart-assistant",
|
||||
"optionsView": "modules/smart-assistant/views/dashlets/options/smart-assistant",
|
||||
"aclScope": "Case"
|
||||
}
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"fields":{"name":{"type":"varchar","maxLength":255,"required":true},"conversationKey":{"type":"varchar","maxLength":255,"required":true},"conversationType":{"type":"enum","options":["office","case"],"default":"office","required":true},"status":{"type":"varchar","maxLength":50,"default":"Active"},"assignedUser":{"type":"link"},"case":{"type":"link"},"caseName":{"type":"varchar","maxLength":255},"messageCount":{"type":"int","default":0},"messagesData":{"type":"jsonArray"},"lastMessageAt":{"type":"datetime"},"lastMessagePreview":{"type":"text","maxLength":200},"createdAt":{"type":"datetime","readOnly":true},"modifiedAt":{"type":"datetime","readOnly":true},"createdBy":{"type":"link","readOnly":true},"modifiedBy":{"type":"link","readOnly":true}},"links":{"assignedUser":{"type":"belongsTo","entity":"User"},"case":{"type":"belongsTo","entity":"Case"},"createdBy":{"type":"belongsTo","entity":"User"},"modifiedBy":{"type":"belongsTo","entity":"User"}},"collection":{"orderBy":"lastMessageAt","order":"desc","textFilterFields":["name","lastMessagePreview"]},"indexes":{"conversationKey":{"columns":["conversationKey","deleted"],"type":"unique"},"assignedUser":{"columns":["assignedUserId","deleted"]},"caseId":{"columns":["caseId","deleted"]},"conversationType":{"columns":["conversationType","deleted"]}}}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"entity":true,"object":true,"module":"SmartAssistant","acl":true,"aclActionList":["create","read","edit","delete"],"aclLevelList":["all","team","own","no"],"tab":false,"stream":false,"disabled":false,"importable":false,"customizable":false}
|
||||
@@ -5,5 +5,6 @@
|
||||
"espocrm": ">=8.0.0"
|
||||
},
|
||||
"author": "klear",
|
||||
"description": "Unified AI Assistant for Legal CRM"
|
||||
"description": "Unified AI Assistant for Legal CRM",
|
||||
"clientModule": "smart-assistant"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
[
|
||||
{
|
||||
"route": "/SmartAssistant/action/status",
|
||||
"method": "get",
|
||||
"params": {
|
||||
"controller": "SmartAssistant",
|
||||
"action": "status"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "/SmartAssistant/action/chat",
|
||||
"method": "post",
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -236,14 +236,27 @@ class SmartAssistantService
|
||||
}
|
||||
}
|
||||
|
||||
public function getStatus(): array
|
||||
{
|
||||
$webhookUrl = $this->getWebhookUrl();
|
||||
|
||||
return [
|
||||
'enabled' => $webhookUrl !== null,
|
||||
'webhookConfigured' => !empty($webhookUrl),
|
||||
];
|
||||
}
|
||||
|
||||
public function getSummary(): array
|
||||
{
|
||||
$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),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -406,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;
|
||||
@@ -421,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;
|
||||
@@ -435,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 [];
|
||||
}
|
||||
|
||||
+9
-5
@@ -1,10 +1,14 @@
|
||||
{
|
||||
"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.0",
|
||||
"acceptableVersions": [">=8.0.0"],
|
||||
"releaseDate": "2026-03-23",
|
||||
"php": [">=8.1"]
|
||||
"version": "1.1.5",
|
||||
"acceptableVersions": [
|
||||
">=8.0.0"
|
||||
],
|
||||
"releaseDate": "2026-03-29",
|
||||
"php": [
|
||||
">=8.1"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user