diff --git a/application/Espo/Core/FieldProcessing/Reminder/Loader.php b/application/Espo/Core/FieldProcessing/Reminder/Loader.php index 8aacfc55c5..d4f300efa4 100644 --- a/application/Espo/Core/FieldProcessing/Reminder/Loader.php +++ b/application/Espo/Core/FieldProcessing/Reminder/Loader.php @@ -29,14 +29,12 @@ namespace Espo\Core\FieldProcessing\Reminder; +use Espo\Entities\User; use Espo\Modules\Crm\Entities\Reminder; -use Espo\ORM\Collection; use Espo\ORM\Entity; - use Espo\Core\FieldProcessing\Loader as LoaderInterface; use Espo\Core\FieldProcessing\Loader\Params; use Espo\Core\ORM\EntityManager; -use stdClass; /** * @internal This class should not be removed as it's used by custom entities. @@ -44,8 +42,10 @@ use stdClass; */ class Loader implements LoaderInterface { - public function __construct(private EntityManager $entityManager) - {} + public function __construct( + private EntityManager $entityManager, + private User $user + ) {} public function process(Entity $entity, Params $params): void { @@ -72,13 +72,14 @@ class Loader implements LoaderInterface { $list = []; - /** @var Collection $collection */ + /** @var iterable $collection */ $collection = $this->entityManager ->getRDBRepository(Reminder::ENTITY_TYPE) ->select(['seconds', 'type']) ->where([ 'entityType' => $entity->getEntityType(), 'entityId' => $entity->getId(), + 'userId' => $this->user->getId(), ]) ->distinct() ->order('seconds') diff --git a/application/Espo/Core/FieldProcessing/Reminder/Saver.php b/application/Espo/Core/FieldProcessing/Reminder/Saver.php index 2c2451baca..8c3cc91539 100644 --- a/application/Espo/Core/FieldProcessing/Reminder/Saver.php +++ b/application/Espo/Core/FieldProcessing/Reminder/Saver.php @@ -29,8 +29,11 @@ namespace Espo\Core\FieldProcessing\Reminder; -use Espo\Core\Utils\DateTime as DateTimeUtil; +use Espo\Core\Field\DateTime; use Espo\Core\Utils\Id\RecordIdGenerator; +use Espo\Core\Utils\Metadata; +use Espo\Entities\Preferences; +use Espo\Entities\User; use Espo\Modules\Crm\Entities\Reminder; use Espo\ORM\Entity; use Espo\Core\ORM\Entity as CoreEntity; @@ -39,8 +42,6 @@ use Espo\Core\FieldProcessing\Saver\Params; use Espo\Core\ORM\EntityManager; use stdClass; -use DateInterval; -use DateTime; /** * @internal This class should not be removed as it's used by custom entities. @@ -53,177 +54,366 @@ class Saver implements SaverInterface public function __construct( private EntityManager $entityManager, - private RecordIdGenerator $idGenerator + private RecordIdGenerator $idGenerator, + private User $user, + private Metadata $metadata ) {} - /** - * @param CoreEntity $entity - */ public function process(Entity $entity, Params $params): void { $entityType = $entity->getEntityType(); - $hasReminder = $this->entityManager - ->getDefs() - ->getEntity($entityType) - ->hasField('reminders'); - - if (!$hasReminder) { + if (!$this->hasRemindersField($entityType)) { return; } - $dateAttribute = $this->entityManager - ->getDefs() - ->getEntity($entityType) - ->getField('reminders') - ->getParam('dateField') ?? - $this->dateAttribute; + $dateAttribute = $this->getDateAttribute($entityType); - $toProcess = - $entity->isNew() || - $entity->isAttributeChanged('assignedUserId') || - ($entity->hasLinkMultipleField('assignedUsers') && $entity->isAttributeChanged('assignedUsersIds')) || - ($entity->hasLinkMultipleField('users') && $entity->isAttributeChanged('usersIds')) || - $entity->isAttributeChanged($dateAttribute) || - $entity->has('reminders'); + if ($this->toRemove($entity)) { + $this->deleteAll($entity); - if (!$toProcess) { return; } - $reminderTypeList = $this->entityManager - ->getDefs() - ->getEntity(Reminder::ENTITY_TYPE) - ->getField('type') - ->getParam('options') ?? []; - - $reminderList = $entity->has('reminders') ? - $entity->get('reminders') : - $this->getEntityReminderDataList($entity); - - if (!$entity->isNew()) { - $query = $this->entityManager - ->getQueryBuilder() - ->delete() - ->from(Reminder::ENTITY_TYPE) - ->where([ - 'entityId' => $entity->getId(), - 'entityType' => $entityType, - 'deleted' => false, - ]) - ->build(); - - $this->entityManager->getQueryExecutor()->execute($query); - } - - if (empty($reminderList)) { + if (!$this->toProcess($entity, $dateAttribute)) { return; } - $dateValue = $entity->get($dateAttribute); + $typeList = $this->getTypeList(); - if (!$entity->has($dateAttribute)) { - $reloadedEntity = $this->entityManager->getEntity($entityType, $entity->getId()); + $onlyRemindersFieldChanged = $this->onlyRemindersFieldChanged($entity, $dateAttribute); - if ($reloadedEntity) { - $dateValue = $reloadedEntity->get($dateAttribute); - } + if (!$entity->isNew() && !$onlyRemindersFieldChanged) { + $this->deleteAll($entity); } - if (!$dateValue) { + if (!$entity->isNew() && $onlyRemindersFieldChanged) { + $this->deleteAllForUser($entity); + } + + $startString = $this->getStartString($entity, $dateAttribute); + + if (!$startString) { return; } - if ($entity->hasLinkMultipleField('users')) { - $userIdList = $entity->getLinkMultipleIdList('users'); - } - else if ($entity->hasLinkMultipleField('assignedUsers')) { - $userIdList = $entity->getLinkMultipleIdList('assignedUsers'); - } - else { - $userIdList = []; + $userIdList = $this->getUserIdList($entity); - if ($entity->get('assignedUserId')) { - $userIdList[] = $entity->get('assignedUserId'); - } - } - - if (empty($userIdList)) { + if ($userIdList === []) { return; } - $dateValueObj = new DateTime($dateValue); + if ($onlyRemindersFieldChanged && in_array($this->user->getId(), $userIdList)) { + $userIdList = [$this->user->getId()]; + } - foreach ($reminderList as $item) { - $remindAt = clone $dateValueObj; - $seconds = intval($item->seconds); - $type = $item->type; + $start = DateTime::fromString($startString); - if (!in_array($type , $reminderTypeList)) { - continue; - } + foreach ($userIdList as $userId) { + $reminderList = $userId === $this->user->getId() ? + $this->getReminderList($entity, $typeList) : + $this->getPreferencesReminderList($typeList, $userId); - $remindAt->sub(new DateInterval('PT' . $seconds . 'S')); - - foreach ($userIdList as $userId) { - $reminderId = $this->idGenerator->generate(); - - $query = $this->entityManager - ->getQueryBuilder() - ->insert() - ->into(Reminder::ENTITY_TYPE) - ->columns([ - 'id', - 'entityId', - 'entityType', - 'type', - 'userId', - 'remindAt', - 'startAt', - 'seconds', - ]) - ->values([ - 'id' => $reminderId, - 'entityId' => $entity->getId(), - 'entityType' => $entityType, - 'type' => $type, - 'userId' => $userId, - 'remindAt' => $remindAt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT), - 'startAt' => $dateValue, - 'seconds' => $seconds, - ]) - ->build(); - - $this->entityManager->getQueryExecutor()->execute($query); + foreach ($reminderList as $item) { + $this->createReminder($entity, $userId, $start, $item); } } } /** - * @return stdClass[] + * @return object{seconds: int, type: string}[] */ - private function getEntityReminderDataList(Entity $entity): array + private function getEntityReminderDataList(CoreEntity $entity): array { - $reminderDataList = []; + $dataList = []; - $reminderCollection = $this->entityManager + /** @var iterable $collection */ + $collection = $this->entityManager ->getRDBRepository(Reminder::ENTITY_TYPE) ->select(['seconds', 'type']) ->where([ 'entityType' => $entity->getEntityType(), 'entityId' => $entity->getId(), + 'userId' => $this->user->getId(), ]) ->distinct() ->order('seconds') ->find(); - foreach ($reminderCollection as $reminder) { - $reminderDataList[] = (object) [ - 'seconds' => $reminder->get('seconds'), - 'type' => $reminder->get('type'), + foreach ($collection as $reminder) { + $dataList[] = (object) [ + 'seconds' => $reminder->getSeconds(), + 'type' => $reminder->getType(), ]; } - return $reminderDataList; + return $dataList; + } + + private function getDateAttribute(string $entityType): string + { + return $this->entityManager + ->getDefs() + ->getEntity($entityType) + ->getField('reminders') + ->getParam('dateField') ?? + $this->dateAttribute; + } + + private function hasRemindersField(string $entityType): bool + { + return $this->entityManager + ->getDefs() + ->getEntity($entityType) + ->hasField('reminders'); + } + + private function isNewOrChanged(CoreEntity $entity, string $dateAttribute): bool + { + return $entity->isNew() || + $this->toReCreate($entity) || + $entity->isAttributeChanged('assignedUserId') || + ($entity->hasLinkMultipleField('assignedUsers') && $entity->isAttributeChanged('assignedUsersIds')) || + ($entity->hasLinkMultipleField('users') && $entity->isAttributeChanged('usersIds')) || + $entity->isAttributeChanged($dateAttribute); + } + + private function toProcess(CoreEntity $entity, string $dateAttribute): bool + { + return $this->isNewOrChanged($entity, $dateAttribute) || $entity->has('reminders'); + } + + private function onlyRemindersFieldChanged(CoreEntity $entity, string $dateAttribute): bool + { + if ($this->isNewOrChanged($entity, $dateAttribute)) { + return false; + } + + return $entity->isAttributeChanged('reminders'); + } + + /** + * @return string[] + */ + private function getTypeList(): array + { + return $this->entityManager + ->getDefs() + ->getEntity(Reminder::ENTITY_TYPE) + ->getField('type') + ->getParam('options') ?? []; + } + + private function deleteAll(CoreEntity $entity): void + { + $query = $this->entityManager + ->getQueryBuilder() + ->delete() + ->from(Reminder::ENTITY_TYPE) + ->where([ + 'entityId' => $entity->getId(), + 'entityType' => $entity->getEntityType(), + ]) + ->build(); + + $this->entityManager->getQueryExecutor()->execute($query); + } + + private function deleteAllForUser(CoreEntity $entity): void + { + $query = $this->entityManager + ->getQueryBuilder() + ->delete() + ->from(Reminder::ENTITY_TYPE) + ->where([ + 'entityId' => $entity->getId(), + 'entityType' => $entity->getEntityType(), + 'userId' => $this->user->getId(), + ]) + ->build(); + + $this->entityManager->getQueryExecutor()->execute($query); + } + + /** + * @return string[] + */ + private function getUserIdList(CoreEntity $entity): array + { + if ($entity->hasLinkMultipleField('users')) { + return $entity->getLinkMultipleIdList('users'); + } + + if ($entity->hasLinkMultipleField('assignedUsers')) { + return $entity->getLinkMultipleIdList('assignedUsers'); + } + + $userIdList = []; + + if ($entity->get('assignedUserId')) { + $userIdList[] = $entity->get('assignedUserId'); + } + + return $userIdList; + } + + private function getStartString(CoreEntity $entity, string $dateAttribute): ?string + { + $dateValue = $entity->get($dateAttribute); + + if (!$entity->has($dateAttribute)) { + $reloadedEntity = $this->entityManager->getEntityById($entity->getEntityType(), $entity->getId()); + + if ($reloadedEntity) { + $dateValue = $reloadedEntity->get($dateAttribute); + } + + } + return $dateValue; + } + + /** + * @param string[] $typeList + * @return object{seconds: int, type: string}[] + */ + private function getReminderList(CoreEntity $entity, array $typeList): array + { + if ($entity->has('reminders')) { + /** @var ?stdClass[] $list */ + $list = $entity->get('reminders'); + + if ($list === null) { + return []; + } + + return $this->sanitizeList($list, $typeList); + } + + return $this->getEntityReminderDataList($entity); + } + + /** + * @param string[] $typeList + * @return object{seconds: int, type: string}[] + */ + private function getPreferencesReminderList(array $typeList, string $userId): array + { + $preferences = $this->entityManager->getRepositoryByClass(Preferences::class)->getById($userId); + + if (!$preferences) { + return []; + } + + /** @var stdClass[] $list */ + $list = $preferences->get('defaultReminders') ?? []; + + return $this->sanitizeList($list, $typeList); + } + + /** + * @param stdClass[] $list + * @param string[] $typeList + * @return object{seconds: int, type: string}[] + */ + private function sanitizeList(array $list, array $typeList): array + { + $result = []; + + foreach ($list as $item) { + $seconds = ($item->seconds ?? null); + $type = ($item->type ?? null); + + if (!is_int($seconds) || !in_array($type, $typeList)) { + continue; + } + + $result[] = (object) [ + 'seconds' => $seconds, + 'type' => $type, + ]; + } + + return $result; + } + + /** + * @param object{seconds: int, type: string} $item + */ + private function createReminder( + CoreEntity $entity, + string $userId, + DateTime $start, + object $item + ): void { + + $seconds = $item->seconds; + $type = $item->type; + + $remindAt = $start->addSeconds(- $seconds); + + $query = $this->entityManager + ->getQueryBuilder() + ->insert() + ->into(Reminder::ENTITY_TYPE) + ->columns([ + 'id', + 'entityId', + 'entityType', + 'type', + 'userId', + 'remindAt', + 'startAt', + 'seconds', + ]) + ->values([ + 'id' => $this->idGenerator->generate(), + 'entityId' => $entity->getId(), + 'entityType' => $entity->getEntityType(), + 'type' => $type, + 'userId' => $userId, + 'remindAt' => $remindAt->toString(), + 'startAt' => $start->toString(), + 'seconds' => $seconds, + ]) + ->build(); + + $this->entityManager->getQueryExecutor()->execute($query); + } + + private function toRemove(CoreEntity $entity): bool + { + if (!$entity->isAttributeChanged('status')) { + return false; + } + + $entityType = $entity->getEntityType(); + + $status = $entity->get('status'); + + $ignoreStatusList = [ + ...($this->metadata->get("scopes.$entityType.completedStatusList") ?? []), + ...($this->metadata->get("scopes.$entityType.canceledStatusList") ?? []), + ]; + + return in_array($status, $ignoreStatusList); + } + + private function toReCreate(CoreEntity $entity): bool + { + if (!$entity->isAttributeChanged('status')) { + return false; + } + + $entityType = $entity->getEntityType(); + + $statusFetched = $entity->getFetched('status'); + $status = $entity->get('status'); + + $ignoreStatusList = [ + ...($this->metadata->get("scopes.$entityType.completedStatusList") ?? []), + ...($this->metadata->get("scopes.$entityType.canceledStatusList") ?? []), + ]; + + return in_array($statusFetched, $ignoreStatusList) && !in_array($status, $ignoreStatusList); } } diff --git a/application/Espo/Core/Templates/Metadata/Event/clientDefs.json b/application/Espo/Core/Templates/Metadata/Event/clientDefs.json index 294ee8decd..35886fe24d 100644 --- a/application/Espo/Core/Templates/Metadata/Event/clientDefs.json +++ b/application/Espo/Core/Templates/Metadata/Event/clientDefs.json @@ -8,6 +8,16 @@ "activitiesCreate": true, "historyCreate": true }, + "viewSetupHandlers": { + "record/detail": [ + "__APPEND__", + "crm:handlers/event/reminders-handler" + ], + "record/edit": [ + "__APPEND__", + "crm:handlers/event/reminders-handler" + ] + }, "bottomPanels": { "detail": [ { @@ -55,19 +65,6 @@ } ] } - }, - "reminders": { - "visible": { - "conditionGroup": [ - { - "type": "isNotEmpty", - "attribute": "assignedUserId", - "data": { - "field": "assignedUser" - } - } - ] - } } } } diff --git a/application/Espo/Core/Templates/Metadata/Event/entityDefs.json b/application/Espo/Core/Templates/Metadata/Event/entityDefs.json index 7bdb65a464..9b502c387c 100644 --- a/application/Espo/Core/Templates/Metadata/Event/entityDefs.json +++ b/application/Espo/Core/Templates/Metadata/Event/entityDefs.json @@ -71,7 +71,8 @@ "validatorClassNameList": [ "Espo\\Modules\\Crm\\Classes\\FieldValidators\\Event\\Reminders\\Valid", "Espo\\Modules\\Crm\\Classes\\FieldValidators\\Event\\Reminders\\MaxCount" - ] + ], + "dynamicLogicDisabled": true }, "createdAt": { "type": "datetime", diff --git a/application/Espo/Modules/Crm/Resources/metadata/clientDefs/Call.json b/application/Espo/Modules/Crm/Resources/metadata/clientDefs/Call.json index 09ea6933b5..f7ebba1889 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/clientDefs/Call.json +++ b/application/Espo/Modules/Crm/Resources/metadata/clientDefs/Call.json @@ -12,6 +12,16 @@ "modalViews": { "detail":"crm:views/meeting/modals/detail" }, + "viewSetupHandlers": { + "record/detail": [ + "__APPEND__", + "crm:handlers/event/reminders-handler" + ], + "record/edit": [ + "__APPEND__", + "crm:handlers/event/reminders-handler" + ] + }, "sidePanels":{ "detail":[ { @@ -115,30 +125,5 @@ "type": "bottomPanelsEditSmall" } }, - "dynamicLogic":{ - "fields":{ - "reminders": { - "visible": { - "conditionGroup": [ - { - "type":"and", - "value": [ - { - "type":"notEquals", - "attribute":"status", - "value":"Held" - }, - { - "type":"notEquals", - "attribute":"status", - "value":"Not Held" - } - ] - } - ] - } - } - } - }, "iconClass": "fas fa-phone" } diff --git a/application/Espo/Modules/Crm/Resources/metadata/clientDefs/Meeting.json b/application/Espo/Modules/Crm/Resources/metadata/clientDefs/Meeting.json index 70add0626a..dad850db47 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/clientDefs/Meeting.json +++ b/application/Espo/Modules/Crm/Resources/metadata/clientDefs/Meeting.json @@ -12,6 +12,16 @@ "modalViews": { "detail":"crm:views/meeting/modals/detail" }, + "viewSetupHandlers": { + "record/detail": [ + "__APPEND__", + "crm:handlers/event/reminders-handler" + ], + "record/edit": [ + "__APPEND__", + "crm:handlers/event/reminders-handler" + ] + }, "sidePanels":{ "detail":[ { @@ -120,28 +130,7 @@ } }, "dynamicLogic":{ - "fields":{ - "reminders": { - "visible": { - "conditionGroup": [ - { - "type":"and", - "value": [ - { - "type":"notEquals", - "attribute":"status", - "value":"Held" - }, - { - "type":"notEquals", - "attribute":"status", - "value":"Not Held" - } - ] - } - ] - } - }, + "fields": { "duration": { "readOnly": { "conditionGroup": [ diff --git a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Call.json b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Call.json index 42c4ad7959..945e79b0ca 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Call.json +++ b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Call.json @@ -53,7 +53,8 @@ "validatorClassNameList": [ "Espo\\Modules\\Crm\\Classes\\FieldValidators\\Event\\Reminders\\Valid", "Espo\\Modules\\Crm\\Classes\\FieldValidators\\Event\\Reminders\\MaxCount" - ] + ], + "dynamicLogicDisabled": true }, "direction": { "type": "enum", diff --git a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Meeting.json b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Meeting.json index 16e43281dc..4fe2b45470 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Meeting.json +++ b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Meeting.json @@ -61,7 +61,8 @@ "validatorClassNameList": [ "Espo\\Modules\\Crm\\Classes\\FieldValidators\\Event\\Reminders\\Valid", "Espo\\Modules\\Crm\\Classes\\FieldValidators\\Event\\Reminders\\MaxCount" - ] + ], + "dynamicLogicDisabled": true }, "description": { "type": "text" diff --git a/client/modules/crm/src/handlers/event/reminders-handler.js b/client/modules/crm/src/handlers/event/reminders-handler.js new file mode 100644 index 0000000000..dfa8fa37b8 --- /dev/null +++ b/client/modules/crm/src/handlers/event/reminders-handler.js @@ -0,0 +1,90 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM – Open Source CRM application. + * Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko + * Website: https://www.espocrm.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * 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. + ************************************************************************/ + +import {Events} from 'bullbone'; + +/** + * @mixes Bull.Events + */ +class RemindersHandler { + + /** + * @param {import('views/record/detail').default} view + */ + constructor(view) { + this.view = view; + /** @type {import('model').default} */ + this.model = view.model; + /** @type {import('models/user').default} */ + this.user = this.view.getUser(); + + this.ignoreStatusList = [ + ...(this.view.getMetadata().get(['scopes', this.view.entityType, 'completedStatusList']) || []), + ...(this.view.getMetadata().get(['scopes', this.view.entityType, 'canceledStatusList']) || []), + ]; + } + + process() { + this.control(); + + this.listenTo(this.model, 'change', () => { + if ( + !this.model.hasChanged('assignedUserId') && + !this.model.hasChanged('usersIds')&& + !this.model.hasChanged('status') + ) { + return; + } + + this.control(); + }); + } + + control() { + const usersIds = /** @type {string[]} */this.model.get('usersIds') || []; + + if ( + !this.ignoreStatusList.includes(this.model.get('status')) && + ( + this.model.get('assignedUserId') === this.user.id || + usersIds.includes(this.user.id) + ) + ) { + this.view.showField('reminders'); + + return; + } + + this.view.hideField('reminders'); + } +} + +Object.assign(RemindersHandler.prototype, Events); + +// noinspection JSUnusedGlobalSymbols +export default RemindersHandler;