From c8f4fa437d9db6187abd944fc7aa3f1edbae5494 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Wed, 17 Jan 2024 12:41:55 +0200 Subject: [PATCH] ref --- .../Crm/Tools/Activities/Api/GetUpcoming.php | 6 +- .../Modules/Crm/Tools/Activities/Service.php | 300 --------------- .../Crm/Tools/Activities/UpcomingService.php | 361 ++++++++++++++++++ 3 files changed, 365 insertions(+), 302 deletions(-) create mode 100644 application/Espo/Modules/Crm/Tools/Activities/UpcomingService.php diff --git a/application/Espo/Modules/Crm/Tools/Activities/Api/GetUpcoming.php b/application/Espo/Modules/Crm/Tools/Activities/Api/GetUpcoming.php index 9b5fae4f97..3ddd8567d4 100644 --- a/application/Espo/Modules/Crm/Tools/Activities/Api/GetUpcoming.php +++ b/application/Espo/Modules/Crm/Tools/Activities/Api/GetUpcoming.php @@ -35,17 +35,19 @@ use Espo\Core\Api\Response; use Espo\Core\Api\ResponseComposer; use Espo\Core\Record\SearchParamsFetcher; use Espo\Entities\User; -use Espo\Modules\Crm\Tools\Activities\Service as Service; +use Espo\Modules\Crm\Tools\Activities\UpcomingService; /** * Upcoming activities. + * + * @noinspection PhpUnused */ class GetUpcoming implements Action { public function __construct( private User $user, private SearchParamsFetcher $searchParamsFetcher, - private Service $service + private UpcomingService $service ) {} public function process(Request $request): Response diff --git a/application/Espo/Modules/Crm/Tools/Activities/Service.php b/application/Espo/Modules/Crm/Tools/Activities/Service.php index 2a631a98dd..d15aebc927 100644 --- a/application/Espo/Modules/Crm/Tools/Activities/Service.php +++ b/application/Espo/Modules/Crm/Tools/Activities/Service.php @@ -39,10 +39,8 @@ use Espo\Core\ServiceFactory; use Espo\Core\Templates\Entities\Company; use Espo\Core\Templates\Entities\Person; use Espo\Core\Utils\Config; -use Espo\Core\Utils\DateTime as DateTimeUtil; use Espo\Core\Utils\Metadata; use Espo\Entities\Email; -use Espo\Entities\Preferences; use Espo\Entities\User; use Espo\Modules\Crm\Entities\Account; use Espo\Modules\Crm\Entities\Call; @@ -50,7 +48,6 @@ use Espo\Modules\Crm\Entities\Contact; use Espo\Modules\Crm\Entities\Lead; use Espo\Modules\Crm\Entities\Meeting; use Espo\Modules\Crm\Entities\Reminder; -use Espo\Modules\Crm\Entities\Task; use Espo\ORM\EntityCollection; use Espo\ORM\EntityManager; use Espo\ORM\Query\UnionBuilder; @@ -59,12 +56,9 @@ use Espo\ORM\Query\SelectBuilder; use Espo\ORM\Entity; use Espo\ORM\Query\Select; use Espo\ORM\Query\Part\Order; - use Espo\Core\Acl\Table; use Espo\Core\Record\Collection as RecordCollection; use Espo\Core\Select\SearchParams; -use Espo\Core\Select\Where\Item as WhereItem; -use Espo\Core\Select\Where\ConverterFactory as WhereConverterFactory; use Espo\Core\Select\SelectBuilderFactory; use Espo\Core\FieldProcessing\ListLoadProcessor; use Espo\Core\FieldProcessing\Loader\Params as FieldLoaderParams; @@ -72,23 +66,17 @@ use Espo\Core\Record\ServiceContainer as RecordServiceContainer; use LogicException; use PDO; -use DateTime; use RuntimeException; class Service { - private const UPCOMING_ACTIVITIES_FUTURE_DAYS = 1; - private const UPCOMING_ACTIVITIES_TASK_FUTURE_DAYS = 7; - /** @var array> */ private array $attributeMap = [ Email::ENTITY_TYPE => [ 'dateSent' => 'dateStart', ], ]; - public function __construct( - private WhereConverterFactory $whereConverterFactory, private ListLoadProcessor $listLoadProcessor, private RecordServiceContainer $recordServiceContainer, private SelectBuilderFactory $selectBuilderFactory, @@ -1160,292 +1148,4 @@ class Service $this->entityManager->getQueryExecutor()->execute($deleteQuery); } - - /** - * @param array{ - * offset?: ?int, - * maxSize?: ?int, - * } $params - * @param ?string[] $entityTypeList - * @return RecordCollection - * @throws Forbidden - * @throws NotFound - * @throws BadRequest - */ - public function getUpcomingActivities( - string $userId, - array $params = [], - ?array $entityTypeList = null, - ?int $futureDays = null - ): RecordCollection { - - /** @var ?User $user */ - $user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $userId); - - if (!$user) { - throw new NotFound(); - } - - $this->accessCheck($user); - - if (!$entityTypeList) { - $entityTypeList = $this->config->get('activitiesEntityList', []); - } - - if (is_null($futureDays)) { - $futureDays = $this->config->get( - 'activitiesUpcomingFutureDays', - self::UPCOMING_ACTIVITIES_FUTURE_DAYS - ); - } - - $queryList = []; - - foreach ($entityTypeList as $entityType) { - if ( - !$this->metadata->get(['scopes', $entityType, 'activity']) && - $entityType !== Task::ENTITY_TYPE - ) { - continue; - } - - if (!$this->acl->checkScope($entityType, 'read')) { - continue; - } - - if (!$this->metadata->get(['entityDefs', $entityType, 'fields', 'dateStart'])) { - continue; - } - - if (!$this->metadata->get(['entityDefs', $entityType, 'fields', 'dateEnd'])) { - continue; - } - - $queryList[] = $this->getUpcomingActivitiesEntityTypeQuery($entityType, $params, $user, $futureDays); - } - - if ($queryList === []) { - return RecordCollection::create(new EntityCollection(), 0); - } - - $builder = $this->entityManager - ->getQueryBuilder() - ->union(); - - foreach ($queryList as $query) { - $builder->query($query); - } - - $unionCountQuery = $builder->build(); - - $countQuery = $this->entityManager->getQueryBuilder() - ->select() - ->fromQuery($unionCountQuery, 'c') - ->select('COUNT:(c.id)', 'count') - ->build(); - - $countSth = $this->entityManager->getQueryExecutor()->execute($countQuery); - - $row = $countSth->fetch(PDO::FETCH_ASSOC); - - $totalCount = $row['count']; - - $offset = intval($params['offset'] ?? 0); - $maxSize = intval($params['maxSize'] ?? 0); - - $unionQuery = $builder - ->order('dateStart') - ->order('dateEnd') - ->order('name') - ->limit($offset, $maxSize) - ->build(); - - $sth = $this->entityManager->getQueryExecutor()->execute($unionQuery); - - $rows = $sth->fetchAll(PDO::FETCH_ASSOC) ?: []; - - $collection = new EntityCollection(); - - foreach ($rows as $row) { - /** @var string $itemEntityType */ - $itemEntityType = $row['entityType']; - /** @var string $itemId */ - $itemId = $row['id']; - - $entity = $this->entityManager->getEntityById($itemEntityType, $itemId); - - if (!$entity) { - // @todo Revise. - $entity = $this->entityManager->getNewEntity($itemEntityType); - - $entity->set('id', $itemId); - } - - $collection->append($entity); - } - - /** @var RecordCollection */ - return RecordCollection::create($collection, $totalCount); - } - - /** - * @param array $params - * @throws Forbidden - * @throws BadRequest - */ - protected function getUpcomingActivitiesEntityTypeQuery( - string $entityType, - array $params, - User $user, - int $futureDays - ): Select { - - $beforeString = (new DateTime()) - ->modify('+' . $futureDays . ' days') - ->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT); - - $builder = $this->selectBuilderFactory - ->create() - ->from($entityType) - ->forUser($user) - ->withBoolFilter('onlyMy') - ->withStrictAccessControl(); - - $primaryFilter = 'planned'; - - if ($entityType === Task::ENTITY_TYPE) { - $primaryFilter = 'actual'; - } - - $builder->withPrimaryFilter($primaryFilter); - - if (!empty($params['textFilter'])) { - $builder->withTextFilter($params['textFilter']); - } - - $queryBuilder = $builder->buildQueryBuilder(); - - $converter = $this->whereConverterFactory->create($entityType, $user); - - $timeZone = $this->getUserTimeZone($user); - - if ($entityType === Task::ENTITY_TYPE) { - $upcomingTaskFutureDays = $this->config->get( - 'activitiesUpcomingTaskFutureDays', - self::UPCOMING_ACTIVITIES_TASK_FUTURE_DAYS - ); - - $taskBeforeString = (new DateTime()) - ->modify('+' . $upcomingTaskFutureDays . ' days') - ->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT); - - $queryBuilder->where([ - 'OR' => [ - [ - 'dateStart' => null, - 'OR' => [ - 'dateEnd' => null, - $converter->convert( - $queryBuilder, - WhereItem::fromRaw([ - 'type' => 'before', - 'attribute' => 'dateEnd', - 'value' => $taskBeforeString, - 'timeZone' => $timeZone, - ]) - )->getRaw() - ] - ], - [ - 'dateStart!=' => null, - 'OR' => [ - $converter->convert( - $queryBuilder, - WhereItem::fromRaw([ - 'type' => 'past', - 'attribute' => 'dateStart', - 'timeZone' => $timeZone, - ]) - )->getRaw(), - $converter->convert( - $queryBuilder, - WhereItem::fromRaw([ - 'type' => 'today', - 'attribute' => 'dateStart', - 'timeZone' => $timeZone, - ]) - )->getRaw(), - $converter->convert( - $queryBuilder, - WhereItem::fromRaw([ - 'type' => 'before', - 'attribute' => 'dateStart', - 'value' => $beforeString, - 'timeZone' => $timeZone, - ]) - )->getRaw(), - ] - ], - ], - ]); - } - else { - $queryBuilder->where([ - 'OR' => [ - $converter->convert( - $queryBuilder, - WhereItem::fromRaw([ - 'type' => 'today', - 'attribute' => 'dateStart', - 'timeZone' => $timeZone, - ]) - )->getRaw(), - [ - $converter->convert( - $queryBuilder, - WhereItem::fromRaw([ - 'type' => 'future', - 'attribute' => 'dateEnd', - 'timeZone' => $timeZone, - ]) - )->getRaw(), - $converter->convert( - $queryBuilder, - WhereItem::fromRaw([ - 'type' => 'before', - 'attribute' => 'dateStart', - 'value' => $beforeString, - 'timeZone' => $timeZone, - ]) - )->getRaw(), - ], - ], - ]); - } - - $queryBuilder->select([ - 'id', - 'name', - 'dateStart', - 'dateEnd', - ['"' . $entityType . '"', 'entityType'], - ]); - - return $queryBuilder->build(); - } - - protected function getUserTimeZone(User $user): string - { - $preferences = $this->entityManager->getEntityById(Preferences::ENTITY_TYPE, $user->getId()); - - if ($preferences) { - $timeZone = $preferences->get('timeZone'); - - if ($timeZone) { - return $timeZone; - } - } - - return $this->config->get('timeZone') ?? 'UTC'; - } } diff --git a/application/Espo/Modules/Crm/Tools/Activities/UpcomingService.php b/application/Espo/Modules/Crm/Tools/Activities/UpcomingService.php new file mode 100644 index 0000000000..464462612c --- /dev/null +++ b/application/Espo/Modules/Crm/Tools/Activities/UpcomingService.php @@ -0,0 +1,361 @@ +. + * + * 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\Tools\Activities; + +use Espo\Core\Acl; +use Espo\Core\Exceptions\BadRequest; +use Espo\Core\Exceptions\NotFound; +use Espo\Core\Exceptions\Forbidden; +use Espo\Core\Utils\Config; +use Espo\Core\Utils\DateTime as DateTimeUtil; +use Espo\Core\Utils\Metadata; +use Espo\Entities\Preferences; +use Espo\Entities\User; +use Espo\Modules\Crm\Entities\Task; +use Espo\ORM\EntityCollection; +use Espo\ORM\EntityManager; +use Espo\ORM\Entity; +use Espo\ORM\Query\Select; +use Espo\Core\Acl\Table; +use Espo\Core\Record\Collection as RecordCollection; +use Espo\Core\Select\Where\Item as WhereItem; +use Espo\Core\Select\Where\ConverterFactory as WhereConverterFactory; +use Espo\Core\Select\SelectBuilderFactory; + +use PDO; +use DateTime; + +class UpcomingService +{ + private const UPCOMING_ACTIVITIES_FUTURE_DAYS = 1; + private const UPCOMING_ACTIVITIES_TASK_FUTURE_DAYS = 7; + + public function __construct( + private WhereConverterFactory $whereConverterFactory, + private SelectBuilderFactory $selectBuilderFactory, + private Config $config, + private Metadata $metadata, + private Acl $acl, + private EntityManager $entityManager + ) {} + /** + * @param array{ + * offset?: ?int, + * maxSize?: ?int, + * } $params + * @param ?string[] $entityTypeList + * @return RecordCollection + * @throws Forbidden + * @throws NotFound + * @throws BadRequest + */ + public function getUpcomingActivities( + string $userId, + array $params = [], + ?array $entityTypeList = null, + ?int $futureDays = null + ): RecordCollection { + + /** @var ?User $user */ + $user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $userId); + + if (!$user) { + throw new NotFound(); + } + + $this->accessCheck($user); + + $entityTypeList ??= $this->config->get('activitiesEntityList', []); + $futureDays ??= $this->config->get('activitiesUpcomingFutureDays', self::UPCOMING_ACTIVITIES_FUTURE_DAYS); + + $queryList = []; + + foreach ($entityTypeList as $entityType) { + if ( + !$this->metadata->get(['scopes', $entityType, 'activity']) && + $entityType !== Task::ENTITY_TYPE + ) { + continue; + } + + if ( + !$this->acl->checkScope($entityType, 'read') || + !$this->metadata->get(['entityDefs', $entityType, 'fields', 'dateStart']) || + !$this->metadata->get(['entityDefs', $entityType, 'fields', 'dateEnd']) + ) { + continue; + } + + $queryList[] = $this->getUpcomingActivitiesEntityTypeQuery($entityType, $params, $user, $futureDays); + } + + if ($queryList === []) { + return RecordCollection::create(new EntityCollection(), 0); + } + + $builder = $this->entityManager + ->getQueryBuilder() + ->union(); + + foreach ($queryList as $query) { + $builder->query($query); + } + + $unionCountQuery = $builder->build(); + + $countQuery = $this->entityManager->getQueryBuilder() + ->select() + ->fromQuery($unionCountQuery, 'c') + ->select('COUNT:(c.id)', 'count') + ->build(); + + $countSth = $this->entityManager->getQueryExecutor()->execute($countQuery); + + $row = $countSth->fetch(PDO::FETCH_ASSOC); + + $totalCount = $row['count']; + + $offset = intval($params['offset'] ?? 0); + $maxSize = intval($params['maxSize'] ?? 0); + + $unionQuery = $builder + ->order('dateStart') + ->order('dateEnd') + ->order('name') + ->limit($offset, $maxSize) + ->build(); + + $sth = $this->entityManager->getQueryExecutor()->execute($unionQuery); + + $rows = $sth->fetchAll(PDO::FETCH_ASSOC) ?: []; + + $collection = new EntityCollection(); + + foreach ($rows as $row) { + /** @var string $itemEntityType */ + $itemEntityType = $row['entityType']; + /** @var string $itemId */ + $itemId = $row['id']; + + $entity = $this->entityManager->getEntityById($itemEntityType, $itemId); + + if (!$entity) { + // @todo Revise. + $entity = $this->entityManager->getNewEntity($itemEntityType); + + $entity->set('id', $itemId); + } + + $collection->append($entity); + } + + /** @var RecordCollection */ + return RecordCollection::create($collection, $totalCount); + } + + /** + * @param array $params + * @throws Forbidden + * @throws BadRequest + */ + private function getUpcomingActivitiesEntityTypeQuery( + string $entityType, + array $params, + User $user, + int $futureDays + ): Select { + + $beforeString = (new DateTime()) + ->modify('+' . $futureDays . ' days') + ->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT); + + $builder = $this->selectBuilderFactory + ->create() + ->from($entityType) + ->forUser($user) + ->withBoolFilter('onlyMy') + ->withStrictAccessControl(); + + $primaryFilter = 'planned'; + + if ($entityType === Task::ENTITY_TYPE) { + $primaryFilter = 'actual'; + } + + $builder->withPrimaryFilter($primaryFilter); + + if (!empty($params['textFilter'])) { + $builder->withTextFilter($params['textFilter']); + } + + $queryBuilder = $builder->buildQueryBuilder(); + + $converter = $this->whereConverterFactory->create($entityType, $user); + + $timeZone = $this->getUserTimeZone($user); + + if ($entityType === Task::ENTITY_TYPE) { + $upcomingTaskFutureDays = $this->config->get( + 'activitiesUpcomingTaskFutureDays', + self::UPCOMING_ACTIVITIES_TASK_FUTURE_DAYS + ); + + $taskBeforeString = (new DateTime()) + ->modify('+' . $upcomingTaskFutureDays . ' days') + ->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT); + + $queryBuilder->where([ + 'OR' => [ + [ + 'dateStart' => null, + 'OR' => [ + 'dateEnd' => null, + $converter->convert( + $queryBuilder, + WhereItem::fromRaw([ + 'type' => 'before', + 'attribute' => 'dateEnd', + 'value' => $taskBeforeString, + 'timeZone' => $timeZone, + ]) + )->getRaw() + ] + ], + [ + 'dateStart!=' => null, + 'OR' => [ + $converter->convert( + $queryBuilder, + WhereItem::fromRaw([ + 'type' => 'past', + 'attribute' => 'dateStart', + 'timeZone' => $timeZone, + ]) + )->getRaw(), + $converter->convert( + $queryBuilder, + WhereItem::fromRaw([ + 'type' => 'today', + 'attribute' => 'dateStart', + 'timeZone' => $timeZone, + ]) + )->getRaw(), + $converter->convert( + $queryBuilder, + WhereItem::fromRaw([ + 'type' => 'before', + 'attribute' => 'dateStart', + 'value' => $beforeString, + 'timeZone' => $timeZone, + ]) + )->getRaw(), + ] + ], + ], + ]); + } + else { + $queryBuilder->where([ + 'OR' => [ + $converter->convert( + $queryBuilder, + WhereItem::fromRaw([ + 'type' => 'today', + 'attribute' => 'dateStart', + 'timeZone' => $timeZone, + ]) + )->getRaw(), + [ + $converter->convert( + $queryBuilder, + WhereItem::fromRaw([ + 'type' => 'future', + 'attribute' => 'dateEnd', + 'timeZone' => $timeZone, + ]) + )->getRaw(), + $converter->convert( + $queryBuilder, + WhereItem::fromRaw([ + 'type' => 'before', + 'attribute' => 'dateStart', + 'value' => $beforeString, + 'timeZone' => $timeZone, + ]) + )->getRaw(), + ], + ], + ]); + } + + $queryBuilder->select([ + 'id', + 'name', + 'dateStart', + 'dateEnd', + ['"' . $entityType . '"', 'entityType'], + ]); + + return $queryBuilder->build(); + } + + private function getUserTimeZone(User $user): string + { + $preferences = $this->entityManager->getEntityById(Preferences::ENTITY_TYPE, $user->getId()); + + if ($preferences) { + $timeZone = $preferences->get('timeZone'); + + if ($timeZone) { + return $timeZone; + } + } + + return $this->config->get('timeZone') ?? 'UTC'; + } + + /** + * @throws Forbidden + */ + private function accessCheck(Entity $entity): void + { + if ($entity instanceof User) { + if (!$this->acl->checkUserPermission($entity)) { + throw new Forbidden(); + } + + return; + } + + if (!$this->acl->check($entity, Table::ACTION_READ)) { + throw new Forbidden(); + } + } +}