diff --git a/application/Espo/Core/Utils/EntityManager.php b/application/Espo/Core/Utils/EntityManager.php index 3b379bb492..89842ab996 100644 --- a/application/Espo/Core/Utils/EntityManager.php +++ b/application/Espo/Core/Utils/EntityManager.php @@ -54,7 +54,7 @@ class EntityManager private $linkForbiddenNameList = ['posts', 'stream', 'subscription']; - private $forbiddenEntityTypeNameList = ['PortalUser', 'ApiUser']; + private $forbiddenEntityTypeNameList = ['PortalUser', 'ApiUser', 'Timeline']; public function __construct(Metadata $metadata, Language $language, File\Manager $fileManager, Config $config, Container $container = null) { diff --git a/application/Espo/Modules/Crm/Controllers/Activities.php b/application/Espo/Modules/Crm/Controllers/Activities.php index edf5b45fa0..8e441253a2 100644 --- a/application/Espo/Modules/Crm/Controllers/Activities.php +++ b/application/Espo/Modules/Crm/Controllers/Activities.php @@ -69,19 +69,58 @@ class Activities extends \Espo\Core\Controllers\Base if ($teamIdList) { $teamIdList = explode(',', $teamIdList); - return $userResultList = $service->getEventsForTeams($teamIdList, $from, $to, $scopeList); + return $userResultList = $service->getTeamsEventList($teamIdList, $from, $to, $scopeList); } if ($userIdList) { $userIdList = explode(',', $userIdList); - return $service->getEventsForUsers($userIdList, $from, $to, $scopeList); + return $service->getUsersEventList($userIdList, $from, $to, $scopeList); } else { if (!$userId) { $userId = $this->getUser()->id; } } - return $service->getEvents($userId, $from, $to, $scopeList); + return $service->getEventList($userId, $from, $to, $scopeList); + } + + public function getActionGetTimeline($params, $data, $request) + { + if (!$this->getAcl()->check('Calendar')) { + throw new Forbidden(); + } + + $from = $request->get('from'); + $to = $request->get('to'); + + if (empty($from) || empty($to)) { + throw new BadRequest(); + } + + if (strtotime($to) - strtotime($from) > $this->maxCalendarRange * 24 * 3600) { + throw new Forbidden('Too long range.'); + } + + $service = $this->getService('Activities'); + + $scopeList = null; + if ($request->get('scopeList') !== null) { + $scopeList = explode(',', $request->get('scopeList')); + } + + $userId = $request->get('userId'); + $userIdList = $request->get('userIdList'); + + if ($userIdList) { + $userIdList = explode(',', $userIdList); + } else { + $userIdList = []; + } + if ($userId) { + $userIdList[] = $userId; + } + + return $service->getUsersTimeline($userIdList, $from, $to, $scopeList); } public function actionListUpcoming($params, $data, $request) diff --git a/application/Espo/Modules/Crm/Resources/routes.json b/application/Espo/Modules/Crm/Resources/routes.json index b916d0789e..8416e8c9ce 100644 --- a/application/Espo/Modules/Crm/Resources/routes.json +++ b/application/Espo/Modules/Crm/Resources/routes.json @@ -18,6 +18,14 @@ "action": "listCalendarEvents" } }, + { + "route": "/Timeline", + "method": "get", + "params": { + "controller": "Activities", + "action": "getTimeline" + } + }, { "route": "/Activities/:scope/:id/:name/list/:entityType", "method": "get", diff --git a/application/Espo/Modules/Crm/Services/Activities.php b/application/Espo/Modules/Crm/Services/Activities.php index 969eaecefa..995a7ef4a0 100644 --- a/application/Espo/Modules/Crm/Services/Activities.php +++ b/application/Espo/Modules/Crm/Services/Activities.php @@ -860,7 +860,7 @@ class Activities extends \Espo\Core\Services\Base return $result; } - protected function getCalendarMeetingQuery($userId, $from, $to) + protected function getCalendarMeetingQuery($userId, $from, $to, $skipAcl) { $selectManager = $this->getSelectManagerFactory()->create('Meeting'); @@ -902,12 +902,14 @@ class Activities extends \Espo\Core\Services\Base 'customJoin' => '' ); - $selectManager->applyAccess($selectParams); + if (!$skipAcl) { + $selectManager->applyAccess($selectParams); + } return $this->getEntityManager()->getQuery()->createSelectQuery('Meeting', $selectParams); } - protected function getCalendarCallQuery($userId, $from, $to) + protected function getCalendarCallQuery($userId, $from, $to, $skipAcl) { $selectManager = $this->getSelectManagerFactory()->create('Call'); @@ -949,12 +951,14 @@ class Activities extends \Espo\Core\Services\Base 'customJoin' => '' ); - $selectManager->applyAccess($selectParams); + if (!$skipAcl) { + $selectManager->applyAccess($selectParams); + } return $this->getEntityManager()->getQuery()->createSelectQuery('Call', $selectParams); } - protected function getCalendarTaskQuery($userId, $from, $to) + protected function getCalendarTaskQuery($userId, $from, $to, $skipAcl = false) { $selectManager = $this->getSelectManagerFactory()->create('Task'); @@ -1002,12 +1006,14 @@ class Activities extends \Espo\Core\Services\Base $selectParams['whereClause'][] = ['assignedUserId' => $userId]; } - $selectManager->applyAccess($selectParams); + if (!$skipAcl) { + $selectManager->applyAccess($selectParams); + } return $this->getEntityManager()->getQuery()->createSelectQuery('Task', $selectParams); } - protected function getCalendarSelectParams($scope, $userId, $from, $to) + protected function getCalendarSelectParams($scope, $userId, $from, $to, $skipAcl = false) { $selectManager = $this->getSelectManagerFactory()->create($scope); @@ -1079,25 +1085,27 @@ class Activities extends \Espo\Core\Services\Base $selectParams['leftJoins'][] = 'assignedUsers'; } - $selectManager->applyAccess($selectParams); + if (!$skipAcl) { + $selectManager->applyAccess($selectParams); + } return $selectParams; } - protected function getCalendarQuery($scope, $userId, $from, $to) + protected function getCalendarQuery($scope, $userId, $from, $to, $skipAcl = false) { $selectManager = $this->getSelectManagerFactory()->create($scope); if (method_exists($selectManager, 'getCalendarSelectParams')) { - $selectParams = $selectManager->getCalendarSelectParams($userId, $from, $to); + $selectParams = $selectManager->getCalendarSelectParams($userId, $from, $to, $skipAcl); return $this->getEntityManager()->getQuery()->createSelectQuery($scope, $selectParams); } $methodName = 'getCalendar' . $scope . 'Query'; if (method_exists($this, $methodName)) { - return $this->$methodName($userId, $from, $to); + return $this->$methodName($userId, $from, $to, $skipAcl); } - $selectParams = $this->getCalendarSelectParams($scope, $userId, $from, $to); + $selectParams = $this->getCalendarSelectParams($scope, $userId, $from, $to, $skipAcl); return $this->getEntityManager()->getQuery()->createSelectQuery($scope, $selectParams); } @@ -1178,7 +1186,35 @@ class Activities extends \Espo\Core\Services\Base return $selectParams; } + public function getUsersTimeline($userIdList, $from, $to, $scopeList = null) + { + $resultData = (object) []; + foreach ($userIdList as $userId) { + $userData = (object) [ + 'eventList' => [], + 'busyRangeList' => [] + ]; + try { + $userData->eventList = $this->getEventList($userId, $from, $to, $scopeList); + $userData->busyRangeList = $this->getBusyRangeList($userId, $from, $to, $scopeList); + } catch (\Exception $e) { + if ($e instanceof Forbidden) { + continue; + } + throw new \Exception($e->getMessage(), $e->getCode(), $e); + } + + $resultData->$userId = $userData; + } + return $resultData; + } + public function getEventsForUsers($userIdList, $from, $to, $scopeList = null) + { + return $this->getUsersEventList($userIdList, $from, $to, $scopeList); + } + + public function getUsersEventList($userIdList, $from, $to, $scopeList = null) { $resultList = []; foreach ($userIdList as $userId) { @@ -1199,6 +1235,11 @@ class Activities extends \Espo\Core\Services\Base } public function getEventsForTeams($teamIdList, $from, $to, $scopeList = null) + { + return $this->getTeamsEventList($teamIdList, $from, $to, $scopeList); + } + + public function getTeamsEventList($teamIdList, $from, $to, $scopeList = null) { if ($this->getAcl()->get('userPermission') === 'no') { throw new Forbidden("User Permission not allowing to view calendars of other users."); @@ -1226,10 +1267,9 @@ class Activities extends \Espo\Core\Services\Base $userNames->{$user->id} = $user->get('name'); } - $eventList = []; foreach ($userIdList as $userId) { - $userEventList = $this->getEvents($userId, $from, $to, $scopeList); + $userEventList = $this->getEventList($userId, $from, $to, $scopeList); foreach ($userEventList as $event) { foreach ($eventList as &$e) { if ($e['scope'] == $event['scope'] && $e['id'] == $event['id']) { @@ -1255,7 +1295,84 @@ class Activities extends \Espo\Core\Services\Base return $eventList; } - public function getEvents($userId, $from, $to, $scopeList = null) + public function getBusyRangeList($userId, $from, $to, $scopeList = null) + { + $rangeList = []; + + $eventList = $this->getEventList($userId, $from, $to, $scopeList, true); + + foreach ($eventList as $i => $item) { + $eventList[$i] = (object) $item; + } + foreach ($eventList as $event) { + if (empty($event->dateStart) || empty($event->dateEnd)) continue; + try { + $start = new \DateTime($event->dateStart); + $end = new \DateTime($event->dateEnd); + + + foreach ($rangeList as &$range) { + if ( + $start->getTimestamp() < $range->start->getTimestamp() + && + $end->getTimestamp() > $range->end->getTimestamp() + ) { + $range->dateStart = $event->dateStart; + $range->start = $start; + $range->dateEnd = $event->dateEnd; + $range->end = $end; + continue 2; + } + + if ( + $start->getTimestamp() < $range->start->getTimestamp() + && + $end->getTimestamp() > $range->start->getTimestamp() + ) { + $range->dateStart = $event->dateStart; + $range->start = $start; + if ($end->getTimestamp() > $range->end->getTimestamp()) { + $range->dateEnd = $event->dateEnd; + $range->end = $end; + } + continue 2; + } + + if ( + $start->getTimestamp() < $range->end->getTimestamp() + && + $end->getTimestamp() > $range->end->getTimestamp() + ) { + $range->dateEnd = $event->dateEnd; + $range->end = $end; + if ($start->getTimestamp() < $range->start->getTimestamp()) { + $range->dateStart = $event->dateStart; + $range->start = $start; + } + continue 2; + } + } + + $busyItem = (object) [ + 'dateStart' => $event->dateStart, + 'dateEnd' => $event->dateEnd, + 'start' => $start, + 'end' => $end + ]; + + $rangeList[] = $busyItem; + } catch (\Exception $e) {} + } + + foreach ($rangeList as &$item) { + unset($item->start); + unset($item->end); + } + + return $rangeList; + } + + public function getEventList($userId, $from, $to, $scopeList = null, $skipAcl = false) { $user = $this->getEntityManager()->getEntity('User', $userId); if (!$user) { @@ -1278,8 +1395,8 @@ class Activities extends \Espo\Core\Services\Base continue; } if ($this->getAcl()->checkScope($scope)) { - if ($this->getMetadata()->get('scopes.' . $scope . '.calendar')) { - $sqlPartList[] = $this->getCalendarQuery($scope, $userId, $from, $to); + if ($this->getMetadata()->get(['scopes', $scope, 'calendar'])) { + $sqlPartList[] = $this->getCalendarQuery($scope, $userId, $from, $to, $skipAcl); } } } @@ -1292,9 +1409,14 @@ class Activities extends \Espo\Core\Services\Base $sth = $pdo->prepare($sql); $sth->execute(); - $rows = $sth->fetchAll(PDO::FETCH_ASSOC); + $rowList = $sth->fetchAll(PDO::FETCH_ASSOC); - return $rows; + return $rowList; + } + + public function getEvents($userId, $from, $to, $scopeList = null, $skipAcl = false) + { + return $this->getEventList($userId, $from, $to, $scopeList, $skipAcl); } public function removeReminder($id) diff --git a/client/modules/crm/src/views/calendar/timeline.js b/client/modules/crm/src/views/calendar/timeline.js index 674e1dc4b2..35f2f6eba5 100644 --- a/client/modules/crm/src/views/calendar/timeline.js +++ b/client/modules/crm/src/views/calendar/timeline.js @@ -317,18 +317,29 @@ Espo.define('crm:views/calendar/timeline', ['view', 'lib!vis'], function (Dep, V convertEvent: function (o) { var userId = o.userId || this.userList[0].id || this.getUser().id; - var event = { - content: o.name, - title: o.name, - id: userId + '-' + o.scope + '-' + o.id, - group: userId, - 'record-id': o.id, - scope: o.scope, - status: o.status, - 'date-start': o.dateStart, - 'date-end': o.dateEnd, - type: 'range' - }; + var event; + if (o.isBusyRange) { + var event = { + className: 'busy', + group: userId, + 'date-start': o.dateStart, + 'date-end': o.dateEnd, + type: 'background' + }; + } else { + event = { + content: o.name, + title: o.name, + id: userId + '-' + o.scope + '-' + o.id, + group: userId, + 'record-id': o.id, + scope: o.scope, + status: o.status, + 'date-start': o.dateStart, + 'date-end': o.dateEnd, + type: 'range' + }; + } this.eventAttributes.forEach(function (attr) { event[attr] = o[attr]; @@ -348,6 +359,10 @@ Espo.define('crm:views/calendar/timeline', ['view', 'lib!vis'], function (Dep, V } } + if (o.isBusyRange) { + return event; + } + if (~this.allDayScopeList.indexOf(o.scope)) { event.type = 'box'; if (event.end) { @@ -405,8 +420,8 @@ Espo.define('crm:views/calendar/timeline', ['view', 'lib!vis'], function (Dep, V convertEventList: function (list) { var resultList = []; - list.forEach(function (o) { - var event = this.convertEvent(o); + list.forEach(function (iten) { + var event = this.convertEvent(iten); if (!event) return; resultList.push(event); }, this); @@ -778,7 +793,7 @@ Espo.define('crm:views/calendar/timeline', ['view', 'lib!vis'], function (Dep, V var fromString = from.utc().format(this.getDateTime().internalDateTimeFormat); var toString = to.utc().format(this.getDateTime().internalDateTimeFormat); - var url = 'Activities?from=' + fromString + '&to=' + toString; + var url = 'Timeline?from=' + fromString + '&to=' + toString; var userIdList = this.userList.map(function (user) { return user.id }, this); @@ -794,9 +809,27 @@ Espo.define('crm:views/calendar/timeline', ['view', 'lib!vis'], function (Dep, V this.ajaxGetRequest(url).then(function (data) { this.fetchedStart = from.clone(); this.fetchedEnd = to.clone(); + var eventList = []; - var eventList = this.convertEventList(data); - callback(eventList); + for (var userId in data) { + var userEventList = data[userId].eventList; + userEventList.forEach(function (item) { + item.userId = userId; + eventList.push(item); + }, this); + + if (userId == this.getUser().id) continue; + + var userBusyRangeList = data[userId].busyRangeList; + userBusyRangeList.forEach(function (item) { + item.userId = userId; + item.isBusyRange = true; + eventList.push(item); + }, this); + } + + var convertedEventList = this.convertEventList(eventList); + callback(convertedEventList); this.notify(false); }.bind(this)); }, diff --git a/frontend/less/espo/misc/timeline/timeline.less b/frontend/less/espo/misc/timeline/timeline.less index 431ff595f0..4e695b0731 100644 --- a/frontend/less/espo/misc/timeline/timeline.less +++ b/frontend/less/espo/misc/timeline/timeline.less @@ -42,11 +42,11 @@ } .vis-item.event-canceled .vis-item-content { - text-decoration: line-through !important; + text-decoration: line-through !important; } .vis-item.vis-range, .vis-item.vis-box { - border-radius: 0 !important; + border-radius: 0 !important; } .vis-current-time { @@ -61,4 +61,8 @@ .calendar-container .timeline .vis-tooltip { display: none; +} + +.vis-item.vis-background.busy { + background-color: @gray-lighter; } \ No newline at end of file