From 8a177d5ac821a8401983aee69cd7b01dca3f524a Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sat, 28 Jun 2025 14:35:06 +0300 Subject: [PATCH] calendar websocket --- .../Espo/Core/Templates/Entities/Event.php | 11 +++ .../Crm/Hooks/Call/CalendarWebSocket.php | 66 +++++++++++++++ .../Crm/Hooks/Common/CalendarWebSocket.php | 83 +++++++++++++++++++ .../Crm/Hooks/Meeting/CalendarWebSocket.php | 66 +++++++++++++++ .../Crm/Hooks/Task/CalendarWebSocket.php | 75 +++++++++++++++++ .../Crm/Resources/metadata/app/webSocket.json | 5 +- .../crm/src/views/calendar/calendar-page.js | 71 +++++++++++++++- 7 files changed, 372 insertions(+), 5 deletions(-) create mode 100644 application/Espo/Modules/Crm/Hooks/Call/CalendarWebSocket.php create mode 100644 application/Espo/Modules/Crm/Hooks/Common/CalendarWebSocket.php create mode 100644 application/Espo/Modules/Crm/Hooks/Meeting/CalendarWebSocket.php create mode 100644 application/Espo/Modules/Crm/Hooks/Task/CalendarWebSocket.php diff --git a/application/Espo/Core/Templates/Entities/Event.php b/application/Espo/Core/Templates/Entities/Event.php index 98a6dfd95b..3e77d32389 100644 --- a/application/Espo/Core/Templates/Entities/Event.php +++ b/application/Espo/Core/Templates/Entities/Event.php @@ -29,6 +29,8 @@ namespace Espo\Core\Templates\Entities; +use Espo\Core\Field\Link; +use Espo\Core\Name\Field; use Espo\Core\ORM\Entity; class Event extends Entity @@ -48,4 +50,13 @@ class Event extends Entity { return $this->set('status', $status); } + + /** + * @since 9.2.0 + */ + public function getAssignedUser(): ?Link + { + /** @var ?Link */ + return $this->getValueObject(Field::ASSIGNED_USER); + } } diff --git a/application/Espo/Modules/Crm/Hooks/Call/CalendarWebSocket.php b/application/Espo/Modules/Crm/Hooks/Call/CalendarWebSocket.php new file mode 100644 index 0000000000..b8eef5bafe --- /dev/null +++ b/application/Espo/Modules/Crm/Hooks/Call/CalendarWebSocket.php @@ -0,0 +1,66 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Modules\Crm\Hooks\Call; + +use Espo\Core\Hook\Hook\AfterRemove; +use Espo\Core\Hook\Hook\AfterSave; +use Espo\Core\WebSocket\Submission; +use Espo\Modules\Crm\Entities\Call; +use Espo\ORM\Entity; +use Espo\ORM\Repository\Option\RemoveOptions; +use Espo\ORM\Repository\Option\SaveOptions; + +/** + * @implements AfterSave + * @implements AfterRemove + */ +class CalendarWebSocket implements AfterSave, AfterRemove +{ + public function __construct( + private Submission $submission, + ) {} + + public function afterSave(Entity $entity, SaveOptions $options): void + { + $this->process($entity); + } + + public function afterRemove(Entity $entity, RemoveOptions $options): void + { + $this->process($entity); + } + + private function process(Call $entity): void + { + foreach ($entity->getUsers()->getIdList() as $userId) { + $this->submission->submit('calendarUpdate', $userId); + } + } +} diff --git a/application/Espo/Modules/Crm/Hooks/Common/CalendarWebSocket.php b/application/Espo/Modules/Crm/Hooks/Common/CalendarWebSocket.php new file mode 100644 index 0000000000..0dbbb74aa5 --- /dev/null +++ b/application/Espo/Modules/Crm/Hooks/Common/CalendarWebSocket.php @@ -0,0 +1,83 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Modules\Crm\Hooks\Common; + +use Espo\Core\Hook\Hook\AfterRemove; +use Espo\Core\Hook\Hook\AfterSave; +use Espo\Core\Name\Field; +use Espo\Core\Templates\Entities\Event; +use Espo\Core\WebSocket\Submission; +use Espo\ORM\Entity; +use Espo\ORM\Repository\Option\RemoveOptions; +use Espo\ORM\Repository\Option\SaveOptions; + +/** + * @implements AfterSave + * @implements AfterRemove + */ +class CalendarWebSocket implements AfterSave, AfterRemove +{ + public function __construct( + private Submission $submission, + ) {} + + public function afterSave(Entity $entity, SaveOptions $options): void + { + if (!$entity instanceof Event) { + return; + } + + $this->process($entity); + } + + public function afterRemove(Entity $entity, RemoveOptions $options): void + { + if (!$entity instanceof Event) { + return; + } + + $this->process($entity); + } + + private function process(Event $entity): void + { + if ($entity->hasLinkMultipleField(Field::ASSIGNED_USERS)) { + foreach ($entity->getLinkMultipleIdList(Field::ASSIGNED_USER) as $userId) { + $this->submission->submit('calendarUpdate', $userId); + } + + return; + } + + if ($entity->getAssignedUser()) { + $this->submission->submit('calendarUpdate', $entity->getAssignedUser()->getId()); + } + } +} diff --git a/application/Espo/Modules/Crm/Hooks/Meeting/CalendarWebSocket.php b/application/Espo/Modules/Crm/Hooks/Meeting/CalendarWebSocket.php new file mode 100644 index 0000000000..e6181b65c1 --- /dev/null +++ b/application/Espo/Modules/Crm/Hooks/Meeting/CalendarWebSocket.php @@ -0,0 +1,66 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Modules\Crm\Hooks\Meeting; + +use Espo\Core\Hook\Hook\AfterRemove; +use Espo\Core\Hook\Hook\AfterSave; +use Espo\Core\WebSocket\Submission; +use Espo\Modules\Crm\Entities\Meeting; +use Espo\ORM\Entity; +use Espo\ORM\Repository\Option\RemoveOptions; +use Espo\ORM\Repository\Option\SaveOptions; + +/** + * @implements AfterSave + * @implements AfterRemove + */ +class CalendarWebSocket implements AfterSave, AfterRemove +{ + public function __construct( + private Submission $submission, + ) {} + + public function afterSave(Entity $entity, SaveOptions $options): void + { + $this->process($entity); + } + + public function afterRemove(Entity $entity, RemoveOptions $options): void + { + $this->process($entity); + } + + private function process(Meeting $entity): void + { + foreach ($entity->getUsers()->getIdList() as $userId) { + $this->submission->submit('calendarUpdate', $userId); + } + } +} diff --git a/application/Espo/Modules/Crm/Hooks/Task/CalendarWebSocket.php b/application/Espo/Modules/Crm/Hooks/Task/CalendarWebSocket.php new file mode 100644 index 0000000000..24ae9da833 --- /dev/null +++ b/application/Espo/Modules/Crm/Hooks/Task/CalendarWebSocket.php @@ -0,0 +1,75 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Modules\Crm\Hooks\Task; + +use Espo\Core\Hook\Hook\AfterRemove; +use Espo\Core\Hook\Hook\AfterSave; +use Espo\Core\Name\Field; +use Espo\Core\WebSocket\Submission; +use Espo\Modules\Crm\Entities\Task; +use Espo\ORM\Entity; +use Espo\ORM\Repository\Option\RemoveOptions; +use Espo\ORM\Repository\Option\SaveOptions; + +/** + * @implements AfterSave + * @implements AfterRemove + */ +class CalendarWebSocket implements AfterSave, AfterRemove +{ + public function __construct( + private Submission $submission, + ) {} + + public function afterSave(Entity $entity, SaveOptions $options): void + { + $this->process($entity); + } + + public function afterRemove(Entity $entity, RemoveOptions $options): void + { + $this->process($entity); + } + + private function process(Task $entity): void + { + if ($entity->hasLinkMultipleField(Field::ASSIGNED_USERS)) { + foreach ($entity->getLinkMultipleIdList(Field::ASSIGNED_USER) as $userId) { + $this->submission->submit('calendarUpdate', $userId); + } + + return; + } + + if ($entity->getAssignedUser()) { + $this->submission->submit('calendarUpdate', $entity->getAssignedUser()->getId()); + } + } +} diff --git a/application/Espo/Modules/Crm/Resources/metadata/app/webSocket.json b/application/Espo/Modules/Crm/Resources/metadata/app/webSocket.json index 7b0eb1d978..44aa029a1c 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/app/webSocket.json +++ b/application/Espo/Modules/Crm/Resources/metadata/app/webSocket.json @@ -1,5 +1,8 @@ { "categories": { - "popupNotifications.event": {} + "popupNotifications.event": {}, + "calendarUpdate": { + "accessCheckCommand": "AclCheck --userId=:userId --scope=Calendar" + } } } diff --git a/client/modules/crm/src/views/calendar/calendar-page.js b/client/modules/crm/src/views/calendar/calendar-page.js index b910bb727e..cd697a6000 100644 --- a/client/modules/crm/src/views/calendar/calendar-page.js +++ b/client/modules/crm/src/views/calendar/calendar-page.js @@ -30,12 +30,13 @@ import View from 'view'; import CalendarEditViewModal from 'crm:views/calendar/modals/edit-view'; import {inject} from 'di'; import {ShortcutManager} from 'helpers/site/shortcut-manager'; +import DebounceHelper from 'helpers/util/debounce'; class CalendarPage extends View { template = 'crm:calendar/calendar-page' - el = '#main' + //el = '#main' fullCalendarModeList = [ 'month', @@ -64,6 +65,18 @@ class CalendarPage extends View { @inject(ShortcutManager) shortcutManager + /** + * @private + * @type {DebounceHelper} + */ + webSocketDebounceHelper + + /** + * @private + * @type {number} + */ + webSocketDebounceInterval = 500 + /** * A shortcut-key => action map. * @@ -149,6 +162,20 @@ class CalendarPage extends View { }, } + /** + * @param {{ + * userId?: string, + * userName?: string|null, + * mode?: string|null, + * date?: string|null, + * }} options + */ + constructor(options) { + super(options); + + this.options = options; + } + setup() { this.mode = this.mode || this.options.mode || null; this.date = this.date || this.options.date || null; @@ -179,16 +206,52 @@ class CalendarPage extends View { this.shortcutManager.add(this, this.shortcutKeys); - this.once('remove', () => { + this.on('remove', () => { this.shortcutManager.remove(this); }); if (!this.mode || ~this.fullCalendarModeList.indexOf(this.mode) || this.mode.indexOf('view-') === 0) { this.setupCalendar(); - } - else if (this.mode === 'timeline') { + } else if (this.mode === 'timeline') { this.setupTimeline(); } + this.initWebSocket(); + } + + /** + * @private + */ + initWebSocket() { + if (this.options.userId && this.getUser().id !== this.options.userId) { + return; + } + + this.webSocketDebounceHelper = new DebounceHelper({ + interval: this.webSocketDebounceInterval, + handler: () => this.handleWebSocketUpdate(), + }); + + if (!this.getHelper().webSocketManager) { + const testHandler = () => this.webSocketDebounceHelper.process(); + + this.on('remove', () => window.removeEventListener('calendar-update', testHandler)); + + // For testing purpose. + window.addEventListener('calendar-update', testHandler); + + return; + } + + this.getHelper().webSocketManager.subscribe('calendarUpdate', () => this.webSocketDebounceHelper.process()); + + this.on('remove', () => this.getHelper().webSocketManager.unsubscribe('calendarUpdate')); + } + + /** + * @private + */ + handleWebSocketUpdate() { + this.getCalendarView()?.actionRefresh({suppressLoadingAlert: true}); } afterRender() {