Compare commits

...

34 Commits

Author SHA1 Message Date
chaim 07df19b412 fix: stream notes showing empty content - add data() method
SmartRequest and SmartAction note views were missing data() method,
so {{post}} in the template resolved to empty string. Added proper
data() with escapeString formatting, consistent with SmartResponse.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 14:09:05 +00:00
chaim b9542e3980 fix: add missing var self = this in showDetail()
self was resolving to window.self instead of the View instance
inside the forEach callback, causing self.escapeString to fail.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:05:30 +00:00
chaim 4f5b6be884 fix: register stream note views in clientDefs.Note.itemViews
EspoCRM resolves note type views via clientDefs.Note.itemViews metadata.
Without this mapping, it looked for SmartRequest/SmartResponse/SmartAction
in the core views path instead of the module path, causing 404 errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:40:48 +00:00
chaim b8e8c01da5 fix: use self.escapeString() for consistency with EspoCRM API
Replace jQuery text escaping with EspoCRM's built-in View.escapeString()
to maintain consistency with the rest of the codebase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:26:19 +00:00
chaim 62004360e8 fix: replace Espo.Utils.escapeString with jQuery text escaping
Espo.Utils.escapeString does not exist in EspoCRM 8.x, causing
TypeError crash when clicking dashlet cards. Use jQuery .text().html()
for XSS-safe HTML escaping instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:53:47 +00:00
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
chaim 1eb856b178 feat: rename assistant to שירה (Shira) 2026-03-31 23:12:31 +00:00
chaim c9d08aa1b9 feat: rename assistant to שירה (Shira) 2026-03-31 23:12:29 +00:00
chaim 233f56eccf feat: add status endpoint, hide FAB/dashlet when integration disabled 2026-03-31 23:10:59 +00:00
chaim 0157b31249 feat: add status endpoint, hide FAB/dashlet when integration disabled 2026-03-31 23:10:33 +00:00
chaim 49c42e3d20 feat: add status endpoint, hide FAB/dashlet when integration disabled 2026-03-31 23:10:29 +00:00
chaim 4dad65f578 feat: add status endpoint, hide FAB/dashlet when integration disabled 2026-03-31 23:10:22 +00:00
chaim f116611c73 feat: add status endpoint, hide FAB/dashlet when integration disabled 2026-03-31 23:09:16 +00:00
chaim ff2388e7e1 feat: add status endpoint, hide FAB/dashlet when integration disabled 2026-03-31 23:09:10 +00:00
chaim 92c3cd2cdc release: v1.0.7 2026-03-31 23:01:33 +00:00
chaim 3102bcff77 feat: clickable dashlet cards with detail drill-down 2026-03-31 23:01:19 +00:00
chaim f5c60f4272 release: v1.0.6 2026-03-31 22:56:28 +00:00
chaim 21c3e33635 fix: add getColor to dashlet for EspoCRM v9 compatibility 2026-03-31 22:56:08 +00:00
chaim ffbb648dd5 release: v1.0.5 2026-03-31 22:53:23 +00:00
chaim f915faafdc fix: add getActionItemDataList to dashlet for EspoCRM v9 compatibility 2026-03-31 22:53:10 +00:00
chaim b9abe5f921 release: v1.0.4 — add AssistantConversation entity 2026-03-31 20:33:21 +00:00
chaim a3801deb55 feat: add AssistantConversation i18n (fa_IR/Hebrew) 2026-03-31 20:33:05 +00:00
chaim 300cc58a97 feat: add AssistantConversation i18n (en_US) 2026-03-31 20:33:01 +00:00
chaim c1ea2b7868 feat: add AssistantConversation scope definition 2026-03-31 20:32:53 +00:00
chaim 184cd19285 feat: add AssistantConversation entity definition for chat persistence 2026-03-31 20:32:49 +00:00
chaim 2c34d56c64 release: v1.0.3 2026-03-29 21:34:38 +00:00
chaim d5c8a60193 fix: add clientModule to module.json for client-side template resolution
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:34:18 +00:00
chaim 96e7c8e9e0 release: v1.0.2 2026-03-29 20:58:35 +00:00
chaim f3805424d3 fix: correct CSS and script paths in app/client.json metadata
Move CSS to module root (not inside src/), fix script paths to include
src/ prefix for handler/view JS files loaded via scriptList.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:58:22 +00:00
chaim 1eea85da2b release: v1.0.1 2026-03-29 20:52:18 +00:00
chaim fbe4f82517 refactor: migrate client paths to EspoCRM module format
Move JS from client/custom/src/modules/{mod}/ to client/custom/modules/{mod}/src/
Move TPL to client/custom/modules/{mod}/res/templates/
Update all define(), createView(), and metadata references from
custom:modules/{mod}/... to modules/{mod}/...

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:51:55 +00:00
chaim 95ca8a0af4 docs: add comprehensive Hebrew README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:53:09 +00:00
chaim bd1cd17490 chore: auto-update README version from manifest on build
build.sh now updates the version in README.md from manifest.json
before creating the ZIP package.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:49:22 +00:00
chaim ffd966a4f0 chore: add composer.json for automated Composer Registry publishing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:12:23 +00:00
30 changed files with 483 additions and 141 deletions
+120
View File
@@ -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.
+5
View File
@@ -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/ \
+14
View File
@@ -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,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,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 &gt; Integrations &gt; 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';
},
});
});
@@ -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();
@@ -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();
@@ -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,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();
@@ -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"}}}
@@ -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"
}
}
@@ -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"
}
@@ -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"]}}}
@@ -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
View File
@@ -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"
]
}