diff --git a/application/Espo/Core/Hooks/Base.php b/application/Espo/Core/Hooks/Base.php index a96b5e2c07..734fb4e2f6 100644 --- a/application/Espo/Core/Hooks/Base.php +++ b/application/Espo/Core/Hooks/Base.php @@ -24,7 +24,7 @@ namespace Espo\Core\Hooks; use \Espo\Core\Interfaces\Injectable; -class Base implements Injectable +abstract class Base implements Injectable { protected $dependencies = array( 'entityManager', diff --git a/application/Espo/Core/Notificators/Base.php b/application/Espo/Core/Notificators/Base.php new file mode 100644 index 0000000000..c94a631ec4 --- /dev/null +++ b/application/Espo/Core/Notificators/Base.php @@ -0,0 +1,98 @@ +init(); + } + + protected function init() + { + } + + public function getDependencyList() + { + return $this->dependencies; + } + + protected function getInjection($name) + { + return $this->injections[$name]; + } + + public function inject($name, $object) + { + $this->injections[$name] = $object; + } + + protected function getEntityManager() + { + return $this->injections['entityManager']; + } + + protected function getUser() + { + return $this->injections['user']; + } + + public function process(Entity $entity) + { + if ($entity->has('assignedUserId') && $entity->get('assignedUserId')) { + $assignedUserId = $entity->get('assignedUserId'); + if ($assignedUserId != $this->getUser()->id && $entity->isFieldChanged('assignedUserId')) { + $notification = $this->getEntityManager()->getEntity('Notification'); + $notification->set(array( + 'type' => 'Assign', + 'userId' => $assignedUserId, + 'data' => array( + 'entityType' => $entity->getEntityType(), + 'entityId' => $entity->id, + 'entityName' => $entity->get('name'), + 'isNew' => $entity->isNew(), + 'userId' => $this->getUser()->id, + 'userName' => $this->getUser()->get('name') + ) + )); + $this->getEntityManager()->saveEntity($notification); + } + } + } + +} + diff --git a/application/Espo/Core/Utils/EntityManager.php b/application/Espo/Core/Utils/EntityManager.php index 6602a47a53..3644937126 100644 --- a/application/Espo/Core/Utils/EntityManager.php +++ b/application/Espo/Core/Utils/EntityManager.php @@ -136,7 +136,8 @@ class EntityManager 'customizable' => true, 'importable' => true, 'type' => $type, - 'stream' => $stream + 'stream' => $stream, + 'notifications' => true ); $this->getMetadata()->set('scopes', $name, $scopeData); diff --git a/application/Espo/Core/defaults/config.php b/application/Espo/Core/defaults/config.php index cf302e1177..2fb0ccaa21 100644 --- a/application/Espo/Core/defaults/config.php +++ b/application/Espo/Core/defaults/config.php @@ -93,6 +93,7 @@ return array ( 'disableExport' => false, 'assignmentEmailNotifications' => false, 'assignmentEmailNotificationsEntityList' => array('Lead', 'Opportunity', 'Task', 'Case'), + 'assignmentNotificationsEntityList' => array('Meeting', 'Call', 'Task', 'Email'), 'emailMessageMaxSize' => 10, 'notificationsCheckInterval' => 10, 'disabledCountQueryEntityList' => array('Email'), diff --git a/application/Espo/Hooks/Common/AssignmentEmailNotification.php b/application/Espo/Hooks/Common/AssignmentEmailNotification.php index e9615cbfe6..fa34b2b7c0 100644 --- a/application/Espo/Hooks/Common/AssignmentEmailNotification.php +++ b/application/Espo/Hooks/Common/AssignmentEmailNotification.php @@ -26,12 +26,13 @@ use Espo\ORM\Entity; class AssignmentEmailNotification extends \Espo\Core\Hooks\Base { - public function afterSave(Entity $entity) { if ( $this->getConfig()->get('assignmentEmailNotifications') && + $entity->has('assignedUserId') + && in_array($entity->getEntityName(), $this->getConfig()->get('assignmentEmailNotificationsEntityList', array())) ) { diff --git a/application/Espo/Hooks/Common/Notifications.php b/application/Espo/Hooks/Common/Notifications.php new file mode 100644 index 0000000000..e9e37fa6e6 --- /dev/null +++ b/application/Espo/Hooks/Common/Notifications.php @@ -0,0 +1,106 @@ +dependencies[] = 'container'; + $this->dependencies[] = 'metadata'; + } + + private $hasStreamCache = array(); + + protected function getContainer() + { + return $this->getInjection('container'); + } + + protected function getMetadata() + { + return $this->getInjection('metadata'); + } + + protected function checkHasStream($entityType) + { + if (!array_key_exists($entityType, $this->hasStreamCache)) { + $this->hasStreamCache[$entityType] = $this->getMetadata()->get("scopes.{$entityType}.stream"); + } + return $this->hasStreamCache[$entityType]; + } + + protected function getNotificator($entityType) + { + if (empty($this->noticatorsHash[$entityType])) { + $normalizedName = Util::normilizeClassName($entityType); + + $className = '\\Espo\\Custom\\Notificators\\' . $normalizedName; + if (!class_exists($className)) { + $moduleName = $this->getMetadata()->getScopeModuleName($entityName); + if ($moduleName) { + $className = '\\Espo\\Modules\\' . $moduleName . '\\Notificators\\' . $normalizedName; + } else { + $className = '\\Espo\\Notificators\\' . $normalizedName; + } + if (!class_exists($className)) { + $className = '\\Espo\\Core\\Notificators\\Base'; + } + } + + $notificator = new $className(); + $dependencies = $notificator->getDependencyList(); + foreach ($dependencies as $name) { + $notificator->inject($name, $this->getContainer()->get($name)); + } + + $this->noticatorsHash[$entityType] = $notificator; + } + return $this->noticatorsHash[$entityType]; + } + + public function afterSave(Entity $entity, array $options = array()) + { + $entityType = $entity->getEntityType(); + + if (!empty($options['silent']) && !empty($options['noNotifications'])) { + return; + } + + if (!$this->checkHasStream($entity)) { + if (in_array($entityType, $this->getConfig()->get('assignmentNotificationsEntityList', []))) { + $notificator = $this->getNotificator(); + $notificator->process($entity); + } + } + } + +} + diff --git a/application/Espo/Hooks/Common/Stream.php b/application/Espo/Hooks/Common/Stream.php index 3549e48fab..416385b3a2 100644 --- a/application/Espo/Hooks/Common/Stream.php +++ b/application/Espo/Hooks/Common/Stream.php @@ -36,6 +36,8 @@ class Stream extends \Espo\Core\Hooks\Base protected $statusFields = null; + public static $order = 9; + protected function init() { $this->dependencies[] = 'serviceFactory'; diff --git a/application/Espo/Modules/Crm/Resources/metadata/scopes/Account.json b/application/Espo/Modules/Crm/Resources/metadata/scopes/Account.json index 3308931398..01dc511f8d 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/scopes/Account.json +++ b/application/Espo/Modules/Crm/Resources/metadata/scopes/Account.json @@ -6,5 +6,6 @@ "module": "Crm", "customizable": true, "stream": true, - "importable": true + "importable": true, + "notifications": true } diff --git a/application/Espo/Modules/Crm/Resources/metadata/scopes/Call.json b/application/Espo/Modules/Crm/Resources/metadata/scopes/Call.json index a8e7a855da..0fc2c29548 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/scopes/Call.json +++ b/application/Espo/Modules/Crm/Resources/metadata/scopes/Call.json @@ -5,5 +5,6 @@ "acl": true, "module": "Crm", "customizable": true, - "importable": true + "importable": true, + "notifications": true } diff --git a/application/Espo/Modules/Crm/Resources/metadata/scopes/Case.json b/application/Espo/Modules/Crm/Resources/metadata/scopes/Case.json index 3308931398..01dc511f8d 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/scopes/Case.json +++ b/application/Espo/Modules/Crm/Resources/metadata/scopes/Case.json @@ -6,5 +6,6 @@ "module": "Crm", "customizable": true, "stream": true, - "importable": true + "importable": true, + "notifications": true } diff --git a/application/Espo/Modules/Crm/Resources/metadata/scopes/Contact.json b/application/Espo/Modules/Crm/Resources/metadata/scopes/Contact.json index 072e7b3d7a..b849647b77 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/scopes/Contact.json +++ b/application/Espo/Modules/Crm/Resources/metadata/scopes/Contact.json @@ -1,5 +1,11 @@ -{"entity":true,"layouts":true,"tab":true,"acl":true,"module":"Crm", +{ + "entity":true, + "layouts":true, + "tab":true, + "acl":true, + "module":"Crm", "customizable": true, "stream": true, - "importable": true + "importable": true, + "notifications": true } diff --git a/application/Espo/Modules/Crm/Resources/metadata/scopes/Document.json b/application/Espo/Modules/Crm/Resources/metadata/scopes/Document.json index 4c0e2bc5a9..44b8fe0b29 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/scopes/Document.json +++ b/application/Espo/Modules/Crm/Resources/metadata/scopes/Document.json @@ -5,5 +5,6 @@ "acl": true, "module": "Crm", "customizable": true, - "importable": false + "importable": false, + "notifications": true } diff --git a/application/Espo/Modules/Crm/Resources/metadata/scopes/Lead.json b/application/Espo/Modules/Crm/Resources/metadata/scopes/Lead.json index 3308931398..01dc511f8d 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/scopes/Lead.json +++ b/application/Espo/Modules/Crm/Resources/metadata/scopes/Lead.json @@ -6,5 +6,6 @@ "module": "Crm", "customizable": true, "stream": true, - "importable": true + "importable": true, + "notifications": true } diff --git a/application/Espo/Modules/Crm/Resources/metadata/scopes/Meeting.json b/application/Espo/Modules/Crm/Resources/metadata/scopes/Meeting.json index a8e7a855da..0fc2c29548 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/scopes/Meeting.json +++ b/application/Espo/Modules/Crm/Resources/metadata/scopes/Meeting.json @@ -5,5 +5,6 @@ "acl": true, "module": "Crm", "customizable": true, - "importable": true + "importable": true, + "notifications": true } diff --git a/application/Espo/Modules/Crm/Resources/metadata/scopes/Opportunity.json b/application/Espo/Modules/Crm/Resources/metadata/scopes/Opportunity.json index 3308931398..01dc511f8d 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/scopes/Opportunity.json +++ b/application/Espo/Modules/Crm/Resources/metadata/scopes/Opportunity.json @@ -6,5 +6,6 @@ "module": "Crm", "customizable": true, "stream": true, - "importable": true + "importable": true, + "notifications": true } diff --git a/application/Espo/Modules/Crm/Resources/metadata/scopes/Target.json b/application/Espo/Modules/Crm/Resources/metadata/scopes/Target.json index bcacda91d0..1c93e4d8a9 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/scopes/Target.json +++ b/application/Espo/Modules/Crm/Resources/metadata/scopes/Target.json @@ -5,5 +5,6 @@ "acl": false, "module": "Crm", "customizable": false, - "importable": false + "importable": false, + "notifications": false } diff --git a/application/Espo/Modules/Crm/Resources/metadata/scopes/TargetList.json b/application/Espo/Modules/Crm/Resources/metadata/scopes/TargetList.json index b6ee9905d4..885e494234 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/scopes/TargetList.json +++ b/application/Espo/Modules/Crm/Resources/metadata/scopes/TargetList.json @@ -6,5 +6,6 @@ "module": "Crm", "customizable": false, "stream": false, - "importable": false + "importable": false, + "notifications": true } diff --git a/application/Espo/Modules/Crm/Resources/metadata/scopes/Task.json b/application/Espo/Modules/Crm/Resources/metadata/scopes/Task.json index a8e7a855da..0fc2c29548 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/scopes/Task.json +++ b/application/Espo/Modules/Crm/Resources/metadata/scopes/Task.json @@ -5,5 +5,6 @@ "acl": true, "module": "Crm", "customizable": true, - "importable": true + "importable": true, + "notifications": true } diff --git a/application/Espo/Notificators/Email.php b/application/Espo/Notificators/Email.php new file mode 100644 index 0000000000..d39febd959 --- /dev/null +++ b/application/Espo/Notificators/Email.php @@ -0,0 +1,83 @@ +isNew()) { + return; + } + + $userIdList = []; + if ($entity->has('assignedUserId') && $entity->get('assignedUserId')) { + $assignedUserId = $entity->get('assignedUserId'); + if ($assignedUserId != $this->getUser()->id && $entity->isFieldChanged('assignedUserId')) { + $userIdList[] = $assignedUserId; + } + } + $emailUserIdList = $email->get('usersIds'); + if (is_null($emailUserIdList)) { + $email->loadLinkMultipleField('from'); + $emailUserIdList = $email->get('usersIds'); + } + if (!is_array($emailUserIdList)) { + $emailUserIdList = []; + } + foreach ($emailUserIdList as $userId) { + if (!in_array($userId, $userIdList)) { + $userIdList[] = $userId; + } + } + + $data = array( + 'emailId' => $entity->id, + 'emailName' => $entity->get('name'), + ); + + $from = $entity->get('from'); + if ($from) { + $person = $this->getEntityManager()->getRepository('EmailAddress')->getEntityByAddress($from); + if ($person) { + $data['personEntityType'] = $person->getEntityName(); + $data['personEntityName'] = $person->get('name'); + $data['personEntityId'] = $person->id; + } + } + + foreach ($userIdList as $userId) { + $notification = $this->getEntityManager()->getEntity('Notification'); + $notification->set(array( + 'type' => 'EmailReceived', + 'userId' => $userId, + 'data' => $data + )); + $this->getEntityManager()->saveEntity($notification); + } + } + +} + diff --git a/application/Espo/Resources/i18n/en_US/Admin.json b/application/Espo/Resources/i18n/en_US/Admin.json index d66c725423..24a018b021 100644 --- a/application/Espo/Resources/i18n/en_US/Admin.json +++ b/application/Espo/Resources/i18n/en_US/Admin.json @@ -44,7 +44,8 @@ "Create Entity": "Create Entity", "Edit Entity": "Edit Entity", "Create Link": "Create Link", - "Edit Link": "Edit Link" + "Edit Link": "Edit Link", + "Notifications": "Notifications" }, "layouts": { "list": "List", @@ -138,7 +139,8 @@ "authentication": "Authentication settings.", "currency": "Currency settings and rates.", "extensions": "Install or uninstall extensions.", - "integrations": "Integration with third-party services." + "integrations": "Integration with third-party services.", + "notifications": "In-app and email notification settings." }, "options": { "previewSize": { diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index 0704d1edfa..16c71f3a0e 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -225,6 +225,10 @@ "dashlets": { "Stream": "Stream" }, + "notificationMessages": { + "assign": "{entityType} {entity} has been assigned to you", + "emailReceived": "Email received from {from}" + }, "streamMessages": { "create": "{user} created {entityType} {entity}", "createAssigned": "{user} created {entityType} {entity} assigned to {assignee}", diff --git a/application/Espo/Resources/i18n/en_US/Settings.json b/application/Espo/Resources/i18n/en_US/Settings.json index 98fa9b52a7..a69a002e66 100644 --- a/application/Espo/Resources/i18n/en_US/Settings.json +++ b/application/Espo/Resources/i18n/en_US/Settings.json @@ -53,8 +53,9 @@ "ldapAccountDomainNameShort": "Account Domain Name Short", "ldapOptReferrals": "Opt Referrals", "disableExport": "Disable Export (only admin is allowed)", + "assignmentNotificationsEntityList": "Entities to Notify about upon Assignment", "assignmentEmailNotifications": "Send Email Notifications upon Assignment", - "assignmentEmailNotificationsEntityList": "Entities to Notify About", + "assignmentEmailNotificationsEntityList": "Entities to Notify about with Email upon Assignment", "b2cMode": "B2C Mode", "disableAvatars": "Disable Avatars" }, @@ -72,7 +73,8 @@ "Locale": "Locale", "SMTP": "SMTP", "Configuration": "Configuration", - "Notifications": "Notifications", + "In-app Notifications": "In-app Notifications", + "Email Notifications": "Email Notifications", "Currency Settings": "Currency Settings", "Currency Rtes": "Currency Rates" } diff --git a/application/Espo/Resources/layouts/Settings/notifications.json b/application/Espo/Resources/layouts/Settings/notifications.json new file mode 100644 index 0000000000..9e89c2df88 --- /dev/null +++ b/application/Espo/Resources/layouts/Settings/notifications.json @@ -0,0 +1,15 @@ +[ + { + "label": "In-app Notifications", + "rows": [ + [{"name": "assignmentNotificationsEntityList"}] + ] + }, + { + "label": "Email Notifications", + "rows": [ + [{"name": "assignmentEmailNotifications"}], + [{"name": "assignmentEmailNotificationsEntityList"}] + ] + } +] \ No newline at end of file diff --git a/application/Espo/Resources/layouts/Settings/outboundEmail.json b/application/Espo/Resources/layouts/Settings/outboundEmail.json index d3e76d802c..6f0803883d 100644 --- a/application/Espo/Resources/layouts/Settings/outboundEmail.json +++ b/application/Espo/Resources/layouts/Settings/outboundEmail.json @@ -14,11 +14,5 @@ [{"name": "outboundEmailFromName"}, {"name": "outboundEmailFromAddress"}], [{"name": "outboundEmailIsShared"}] ] - }, - { - "label": "Notifications", - "rows": [ - [{"name": "assignmentEmailNotifications"}, {"name": "assignmentEmailNotificationsEntityList"}] - ] } ] diff --git a/application/Espo/Resources/metadata/app/adminPanel.json b/application/Espo/Resources/metadata/app/adminPanel.json index 3c079a5427..52ce20244d 100644 --- a/application/Espo/Resources/metadata/app/adminPanel.json +++ b/application/Espo/Resources/metadata/app/adminPanel.json @@ -22,6 +22,11 @@ "label":"Currency", "description":"currency" }, + { + "url":"#Admin/notifications", + "label":"Notifications", + "description":"notifications" + }, { "url":"#Admin/integrations", "label":"Integrations", diff --git a/application/Espo/Resources/metadata/entityDefs/Settings.json b/application/Espo/Resources/metadata/entityDefs/Settings.json index 8e7d4c35a0..ead51ac935 100644 --- a/application/Espo/Resources/metadata/entityDefs/Settings.json +++ b/application/Espo/Resources/metadata/entityDefs/Settings.json @@ -214,6 +214,11 @@ "translation": "Global.scopeNamesPlural", "view": "Settings.Fields.AssignmentEmailNotificationsEntityList" }, + "assignmentNotificationsEntityList": { + "type": "multiEnum", + "translation": "Global.scopeNamesPlural", + "view": "Settings.Fields.AssignmentNotificationsEntityList" + }, "b2cMode": { "type": "bool", "default": false diff --git a/application/Espo/Resources/metadata/scopes/Email.json b/application/Espo/Resources/metadata/scopes/Email.json index 6f6b169b19..e5cbee6128 100644 --- a/application/Espo/Resources/metadata/scopes/Email.json +++ b/application/Espo/Resources/metadata/scopes/Email.json @@ -2,5 +2,6 @@ "entity": true, "layouts": false, "tab": true, - "acl": true + "acl": true, + "notifications": true } diff --git a/application/Espo/Services/Import.php b/application/Espo/Services/Import.php index 1865809419..fc6e374b13 100644 --- a/application/Espo/Services/Import.php +++ b/application/Espo/Services/Import.php @@ -470,7 +470,7 @@ class Import extends \Espo\Services\Record try { $isDuplicate = $recordService->checkEntityForDuplicate($entity); - if ($this->getEntityManager()->saveEntity($entity, array('noStream' => true))) { + if ($this->getEntityManager()->saveEntity($entity, array('noStream' => true, 'noNotifications' => true))) { $result['id'] = $entity->id; if (empty($id)) { $result['isImported'] = true; diff --git a/frontend/client/res/templates/admin/settings/header-notifications.tpl b/frontend/client/res/templates/admin/settings/header-notifications.tpl new file mode 100644 index 0000000000..5e5b3ed6c3 --- /dev/null +++ b/frontend/client/res/templates/admin/settings/header-notifications.tpl @@ -0,0 +1 @@ +