diff --git a/application/Espo/Modules/Crm/Business/Event/Ics.php b/application/Espo/Modules/Crm/Business/Event/Ics.php index 92ce7c3bd6..839dd6e977 100644 --- a/application/Espo/Modules/Crm/Business/Event/Ics.php +++ b/application/Espo/Modules/Crm/Business/Event/Ics.php @@ -33,25 +33,27 @@ use RuntimeException; class Ics { + public const STATUS_CONFIRMED = 'CONFIRMED'; + public const STATUS_TENTATIVE = 'TENTATIVE'; + public const STATUS_CANCELLED = 'CANCELLED'; + + public const METHOD_REQUEST = 'REQUEST'; + public const METHOD_CANCEL = 'CANCEL'; + + /** @var self::METHOD_* string */ + private string $method; private ?string $output = null; - private string $prodid; - private ?int $startDate = null; - private ?int $endDate = null; - private ?string $summary = null; - private ?string $address = null; - private ?string $email = null; - private ?string $who = null; - private ?string $description = null; - private ?string $uid = null; + /** @var self::STATUS_* string */ + private string $status; /** * @param array $attributes @@ -63,6 +65,8 @@ class Ics throw new RuntimeException('PRODID is required'); } + $this->status = self::STATUS_CONFIRMED; + $this->method = self::METHOD_REQUEST; $this->prodid = $prodid; foreach ($attributes as $key => $value) { @@ -90,7 +94,7 @@ class Ics "BEGIN:VCALENDAR\r\n". "VERSION:2.0\r\n". "PRODID:-" . $this->prodid . "\r\n". - "METHOD:REQUEST\r\n". + "METHOD:" . $this->method . "\r\n". "BEGIN:VEVENT\r\n". "DTSTART:" . $this->formatTimestamp($this->startDate) . "\r\n". "DTEND:" . $this->formatTimestamp($this->endDate) . "\r\n". @@ -100,7 +104,8 @@ class Ics "DESCRIPTION:" . $this->escapeString($this->formatMultiline($this->description)) . "\r\n". "UID:" . $this->uid . "\r\n". "SEQUENCE:0\r\n". - "DTSTAMP:" . $this->formatTimestamp(time())."\r\n". + "DTSTAMP:" . $this->formatTimestamp(time()) . "\r\n". + "STATUS:" . $this->status . "\r\n" . "END:VEVENT\r\n". "END:VCALENDAR"; } diff --git a/application/Espo/Modules/Crm/Business/Event/Invitations.php b/application/Espo/Modules/Crm/Business/Event/Invitations.php index 1fd0c50232..0184684fd1 100644 --- a/application/Espo/Modules/Crm/Business/Event/Invitations.php +++ b/application/Espo/Modules/Crm/Business/Event/Invitations.php @@ -54,6 +54,9 @@ use DateTime; class Invitations { + private const TYPE_INVITATION = 'invitation'; + private const TYPE_CANCELLATION = 'cancellation'; + private $smtpParams; private $entityManager; private $emailSender; @@ -97,6 +100,125 @@ class Invitations * @throws Error */ public function sendInvitation(Entity $entity, Entity $invitee, string $link): void + { + $this->sendInternal($entity, $invitee, $link); + } + + /** + * @throws SendingError + * @throws Error + */ + public function sendCancellation(Entity $entity, Entity $invitee, string $link): void + { + $this->sendInternal($entity, $invitee, $link, self::TYPE_CANCELLATION); + } + + /** + * @throws SendingError + * @throws Error + */ + private function sendInternal( + Entity $entity, + Entity $invitee, + string $link, + string $type = self::TYPE_INVITATION + ): void { + + $uid = $type === self::TYPE_INVITATION ? + $this->createUniqueId($entity, $invitee, $link) : null; + + $emailAddress = $invitee->get('emailAddress'); + + if (empty($emailAddress)) { + return; + } + + /** @var Email $email */ + $email = $this->entityManager->getNewEntity(Email::ENTITY_TYPE); + + $email->set('to', $emailAddress); + + $subjectTpl = $this->templateFileManager->getTemplate($type, 'subject', $entity->getEntityType(), 'Crm'); + $bodyTpl = $this->templateFileManager->getTemplate($type, 'body', $entity->getEntityType(), 'Crm'); + + $subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl); + + $data = []; + + $siteUrl = rtrim($this->config->get('siteUrl'), '/'); + $recordUrl = $siteUrl . '/#' . $entity->getEntityType() . '/view/' . $entity->getId(); + + $data['recordUrl'] = $recordUrl; + + if ($uid) { + $part = $siteUrl . '?entryPoint=eventConfirmation&action='; + + $data['acceptLink'] = $part . 'accept&uid=' . $uid->getIdValue(); + $data['declineLink'] = $part . 'decline&uid=' . $uid->getIdValue(); + $data['tentativeLink'] = $part . 'tentative&uid=' . $uid->getIdValue(); + } + + if ($invitee instanceof User) { + $data['isUser'] = true; + + $htmlizer = $this->htmlizerFactory->createForUser($invitee); + } + else { + $htmlizer = $this->htmlizerFactory->createNoAcl(); + } + + $data['inviteeName'] = $invitee->get('name'); + $data['entityType'] = $this->language->translateLabel($entity->getEntityType(), 'scopeNames'); + $data['entityTypeLowerFirst'] = Util::mbLowerCaseFirst($data['entityType']); + + $subject = $htmlizer->render( + $entity, + $subjectTpl, + $type . '-email-subject-' . $entity->getEntityType(), + $data, + true, + true + ); + + $body = $htmlizer->render( + $entity, + $bodyTpl, + $type . '-email-body-' . $entity->getEntityType(), + $data, + false, + true + ); + + $email->set('subject', $subject); + $email->set('body', $body); + $email->set('isHtml', true); + + $attachmentName = ucwords($this->language->translateLabel($entity->getEntityType(), 'scopeNames')) . '.ics'; + + /** @var Attachment $attachment */ + $attachment = $this->entityManager->getNewEntity(Attachment::ENTITY_TYPE); + + $attachment->set([ + 'name' => $attachmentName, + 'type' => 'text/calendar', + 'contents' => $this->getIcsContents($entity, $type), + ]); + + $message = new Message(); + + $sender = $this->emailSender->create(); + + if ($this->smtpParams) { + $sender->withSmtpParams($this->smtpParams); + } + + $sender + ->withMessage($message) + ->withAttachments([$attachment]) + ->send($email); + } + + private function createUniqueId(Entity $entity, Entity $invitee, string $link): UniqueId { /** @var UniqueId $uid */ $uid = $this->entityManager->getNewEntity(UniqueId::ENTITY_TYPE); @@ -128,93 +250,10 @@ class Invitations $this->entityManager->saveEntity($uid); - $emailAddress = $invitee->get('emailAddress'); - - if (empty($emailAddress)) { - return; - } - - /** @var Email $email */ - $email = $this->entityManager->getNewEntity(Email::ENTITY_TYPE); - - $email->set('to', $emailAddress); - - $subjectTpl = $this->templateFileManager->getTemplate('invitation', 'subject', $entity->getEntityType(), 'Crm'); - $bodyTpl = $this->templateFileManager->getTemplate('invitation', 'body', $entity->getEntityType(), 'Crm'); - - $subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl); - - $data = []; - - $siteUrl = rtrim($this->config->get('siteUrl'), '/'); - $recordUrl = $siteUrl . '/#' . $entity->getEntityType() . '/view/' . $entity->getId(); - - $data['recordUrl'] = $recordUrl; - $data['acceptLink'] = $siteUrl . '?entryPoint=eventConfirmation&action=accept&uid=' . $uid->get('name'); - $data['declineLink'] = $siteUrl . '?entryPoint=eventConfirmation&action=decline&uid=' . $uid->get('name'); - $data['tentativeLink'] = $siteUrl . '?entryPoint=eventConfirmation&action=tentative&uid=' . $uid->get('name'); - - if ($invitee instanceof User) { - $data['isUser'] = true; - - $htmlizer = $this->htmlizerFactory->createForUser($invitee); - } - else { - $htmlizer = $this->htmlizerFactory->createNoAcl(); - } - - $data['inviteeName'] = $invitee->get('name'); - $data['entityType'] = $this->language->translateLabel($entity->getEntityType(), 'scopeNames'); - $data['entityTypeLowerFirst'] = Util::mbLowerCaseFirst($data['entityType']); - - $subject = $htmlizer->render( - $entity, - $subjectTpl, - 'invitation-email-subject-' . $entity->getEntityType(), - $data, - true, - true - ); - - $body = $htmlizer->render( - $entity, - $bodyTpl, - 'invitation-email-body-' . $entity->getEntityType(), - $data, - false, - true - ); - - $email->set('subject', $subject); - $email->set('body', $body); - $email->set('isHtml', true); - - $attachmentName = ucwords($this->language->translateLabel($entity->getEntityType(), 'scopeNames')) . '.ics'; - - /** @var Attachment $attachment */ - $attachment = $this->entityManager->getNewEntity(Attachment::ENTITY_TYPE); - - $attachment->set([ - 'name' => $attachmentName, - 'type' => 'text/calendar', - 'contents' => $this->getIcsContents($entity), - ]); - - $message = new Message(); - - $sender = $this->emailSender->create(); - - if ($this->smtpParams) { - $sender->withSmtpParams($this->smtpParams); - } - - $sender - ->withMessage($message) - ->withAttachments([$attachment]) - ->send($email); + return $uid; } - protected function getIcsContents(Entity $entity): string + protected function getIcsContents(Entity $entity, string $type): string { /** @var ?User $user */ $user = $this->entityManager @@ -230,7 +269,16 @@ class Invitations $email = $user->getEmailAddress(); } + $status = $type === self::TYPE_CANCELLATION ? + Ics::STATUS_CANCELLED : + Ics::STATUS_CONFIRMED; + + $method = $type === self::TYPE_CANCELLATION ? + Ics::METHOD_CANCEL : + Ics::METHOD_REQUEST; + $ics = new Ics('//EspoCRM//EspoCRM Calendar//EN', [ + 'method' => $method, 'startDate' => strtotime($entity->get('dateStart')), 'endDate' => strtotime($entity->get('dateEnd')), 'uid' => $entity->getId(), @@ -238,6 +286,7 @@ class Invitations 'who' => $who, 'email' => $email, 'description' => $entity->get('description'), + 'status' => $status, ]); return $ics->get(); diff --git a/application/Espo/Modules/Crm/Controllers/Call.php b/application/Espo/Modules/Crm/Controllers/Call.php index 474b5b0122..d976e1d3ff 100644 --- a/application/Espo/Modules/Crm/Controllers/Call.php +++ b/application/Espo/Modules/Crm/Controllers/Call.php @@ -34,7 +34,6 @@ use Espo\Core\Controllers\Record; use Espo\Core\Exceptions\Error; use Espo\Core\Exceptions\Forbidden; use Espo\Core\Exceptions\BadRequest; -use Espo\Core\Exceptions\ForbiddenSilent; use Espo\Core\Exceptions\NotFound; use Espo\Core\Api\Request; use Espo\Core\Mail\Exceptions\SendingError; @@ -71,6 +70,30 @@ class Call extends Record return $resultList !== 0; } + /** + * @throws BadRequest + * @throws Forbidden + * @throws Error + * @throws SendingError + * @throws NotFound + */ + public function postActionSendCancellation(Request $request): bool + { + $id = $request->getParsedBody()->id ?? null; + + if (!$id) { + throw new BadRequest(); + } + + $invitees = $this->fetchInvitees($request); + + $resultList = $this->injectableFactory + ->create(InvitationService::class) + ->sendCancellation(CallEntity::ENTITY_TYPE, $id, $invitees); + + return $resultList !== 0; + } + /** * @param Request $request * @return ?Invitee[] diff --git a/application/Espo/Modules/Crm/Controllers/Meeting.php b/application/Espo/Modules/Crm/Controllers/Meeting.php index ff435dddd9..77c99e0e4d 100644 --- a/application/Espo/Modules/Crm/Controllers/Meeting.php +++ b/application/Espo/Modules/Crm/Controllers/Meeting.php @@ -73,6 +73,30 @@ class Meeting extends Record return $resultList !== 0; } + /** + * @throws BadRequest + * @throws Forbidden + * @throws Error + * @throws SendingError + * @throws NotFound + */ + public function postActionSendCancellation(Request $request): bool + { + $id = $request->getParsedBody()->id ?? null; + + if (!$id) { + throw new BadRequest(); + } + + $invitees = $this->fetchInvitees($request); + + $resultList = $this->injectableFactory + ->create(InvitationService::class) + ->sendCancellation(MeetingEntity::ENTITY_TYPE, $id, $invitees); + + return $resultList !== 0; + } + /** * @param Request $request * @return ?Invitee[] diff --git a/application/Espo/Modules/Crm/Resources/i18n/en_US/Admin.json b/application/Espo/Modules/Crm/Resources/i18n/en_US/Admin.json index d9bc265b2a..0a5a1930e1 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/en_US/Admin.json +++ b/application/Espo/Modules/Crm/Resources/i18n/en_US/Admin.json @@ -6,6 +6,7 @@ }, "templates": { "invitation": "Invitation", + "cancellation": "Cancellation", "reminder": "Reminder" } } diff --git a/application/Espo/Modules/Crm/Resources/i18n/en_US/Meeting.json b/application/Espo/Modules/Crm/Resources/i18n/en_US/Meeting.json index a9cd9a183a..53a76f2f95 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/en_US/Meeting.json +++ b/application/Espo/Modules/Crm/Resources/i18n/en_US/Meeting.json @@ -42,6 +42,7 @@ "Set Held": "Set Held", "Set Not Held": "Set Not Held", "Send Invitations": "Send Invitations", + "Send Cancellation": "Send Cancellation", "on time": "on time", "before": "before", "All-Day": "All-Day", @@ -54,6 +55,7 @@ }, "messages": { "sendInvitationsToSelectedAttendees": "Invitation emails will be sent to the selected attendees.", + "sendCancellationsToSelectedAttendees": "Cancellation emails will be sent to the selected attendees.", "selectAcceptanceStatus": "Set your acceptance status.", "nothingHasBeenSent": "Nothing were sent" } diff --git a/application/Espo/Modules/Crm/Resources/metadata/app/templates.json b/application/Espo/Modules/Crm/Resources/metadata/app/templates.json index 202204af77..d759e0106f 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/app/templates.json +++ b/application/Espo/Modules/Crm/Resources/metadata/app/templates.json @@ -3,8 +3,12 @@ "scopeList": ["Meeting", "Call"], "module": "Crm" }, + "cancellation": { + "scopeList": ["Meeting", "Call"], + "module": "Crm" + }, "reminder": { "scopeList": ["Meeting", "Call", "Task"], "module": "Crm" } -} \ No newline at end of file +} diff --git a/application/Espo/Modules/Crm/Resources/templates/cancellation/en_US/body.tpl b/application/Espo/Modules/Crm/Resources/templates/cancellation/en_US/body.tpl new file mode 100644 index 0000000000..6b313a7699 --- /dev/null +++ b/application/Espo/Modules/Crm/Resources/templates/cancellation/en_US/body.tpl @@ -0,0 +1,5 @@ +

Subject: {{name}}

+

Start: {{#if isAllDay}}{{dateStartDate}}{{else}}{{dateStart}}{{/if}}

+{{#if isUser}} +

View record

+{{/if}} diff --git a/application/Espo/Modules/Crm/Resources/templates/cancellation/en_US/subject.tpl b/application/Espo/Modules/Crm/Resources/templates/cancellation/en_US/subject.tpl new file mode 100644 index 0000000000..83103d4b8f --- /dev/null +++ b/application/Espo/Modules/Crm/Resources/templates/cancellation/en_US/subject.tpl @@ -0,0 +1 @@ +Cancellation of {{entityTypeLowerFirst}} '{{name}}' diff --git a/application/Espo/Modules/Crm/Tools/Meeting/InvitationService.php b/application/Espo/Modules/Crm/Tools/Meeting/InvitationService.php index 6ae6cfe371..2b11be1246 100644 --- a/application/Espo/Modules/Crm/Tools/Meeting/InvitationService.php +++ b/application/Espo/Modules/Crm/Tools/Meeting/InvitationService.php @@ -46,9 +46,11 @@ use Espo\ORM\Entity; use Espo\ORM\EntityManager; use Espo\Tools\Email\SendService; - class InvitationService { + private const TYPE_INVITATION = 'invitation'; + private const TYPE_CANCELLATION = 'cancellation'; + private RecordServiceContainer $recordServiceContainer; private SendService $sendService; private User $user; @@ -76,7 +78,7 @@ class InvitationService } /** - * Send invitations for a meeting (or call). Checks access. Uses user's SMTP if available. + * Send invitation emails for a meeting (or call). Checks access. Uses user's SMTP if available. * * @param ?Invitee[] $targets * @return Entity[] Entities an invitation was sent to. @@ -87,6 +89,39 @@ class InvitationService */ public function send(string $entityType, string $id, ?array $targets = null): array { + return $this->sendInternal($entityType, $id, $targets); + } + + /** + * Send cancellation emails for a meeting (or call). Checks access. Uses user's SMTP if available. + * + * @param ?Invitee[] $targets + * @return Entity[] Entities a cancellation was sent to. + * @throws NotFound + * @throws Forbidden + * @throws Error + * @throws SendingError + */ + public function sendCancellation(string $entityType, string $id, ?array $targets = null): array + { + return $this->sendInternal($entityType, $id, $targets, self::TYPE_CANCELLATION); + } + + /** + * @param ?Invitee[] $targets + * @return Entity[] + * @throws NotFound + * @throws Forbidden + * @throws Error + * @throws SendingError + */ + private function sendInternal( + string $entityType, + string $id, + ?array $targets = null, + string $type = self::TYPE_INVITATION + ): array { + $entity = $this->recordServiceContainer ->get($entityType) ->getEntity($id); @@ -115,7 +150,7 @@ class InvitationService ->getRDBRepository($entityType) ->getRelation($entity, $link); - if ($targets === null) { + if ($targets === null && $type === self::TYPE_INVITATION) { $builder->where([ '@relation.status=' => Meeting::ATTENDEE_STATUS_NONE, ]); @@ -134,7 +169,13 @@ class InvitationService continue; } - $sender->sendInvitation($entity, $attendee, $link); + if ($type === self::TYPE_INVITATION) { + $sender->sendInvitation($entity, $attendee, $link); + } + + if ($type === self::TYPE_CANCELLATION) { + $sender->sendCancellation($entity, $attendee, $link); + } $sentAddressList[] = $emailAddress; $resultEntityList[] = $attendee; diff --git a/client/modules/crm/src/views/call/detail.js b/client/modules/crm/src/views/call/detail.js index 0737bd49d4..17e4bfdb24 100644 --- a/client/modules/crm/src/views/call/detail.js +++ b/client/modules/crm/src/views/call/detail.js @@ -30,25 +30,35 @@ return Dep.extend({ + cancellationPeriod: '8 hours', + setup: function () { Dep.prototype.setup.call(this); this.controlSendInvitationsButton(); this.controlAcceptanceStatusButton(); + this.controlSendCancellationButton(); this.listenTo(this.model, 'sync', () => { this.controlSendInvitationsButton(); + this.controlSendCancellationButton(); }); this.listenTo(this.model, 'sync', () => { this.controlAcceptanceStatusButton(); }); + + MeetingDetail.prototype.setupCancellationPeriod.call(this); }, actionSendInvitations: function () { MeetingDetail.prototype.actionSendInvitations.call(this); }, + actionSendCancellation: function () { + MeetingDetail.prototype.actionSendCancellation.call(this); + }, + actionSetAcceptanceStatus: function () { MeetingDetail.prototype.actionSetAcceptanceStatus.call(this); }, @@ -57,6 +67,10 @@ MeetingDetail.prototype.controlSendInvitationsButton.call(this); }, + controlSendCancellationButton: function () { + MeetingDetail.prototype.controlSendCancellationButton.call(this); + }, + controlAcceptanceStatusButton: function () { MeetingDetail.prototype.controlAcceptanceStatusButton.call(this); }, diff --git a/client/modules/crm/src/views/meeting/detail.js b/client/modules/crm/src/views/meeting/detail.js index dddc5159d7..3a598b6111 100644 --- a/client/modules/crm/src/views/meeting/detail.js +++ b/client/modules/crm/src/views/meeting/detail.js @@ -30,19 +30,41 @@ define('crm:views/meeting/detail', ['views/detail', 'lib!moment'], function (Dep return Dep.extend({ + cancellationPeriod: '8 hours', + setup: function () { Dep.prototype.setup.call(this); this.controlSendInvitationsButton(); this.controlAcceptanceStatusButton(); + this.controlSendCancellationButton(); this.listenTo(this.model, 'sync', () => { this.controlSendInvitationsButton(); + this.controlSendCancellationButton(); }); this.listenTo(this.model, 'sync', () => { this.controlAcceptanceStatusButton(); }); + + this.setupCancellationPeriod(); + }, + + setupCancellationPeriod: function () { + this.cancellationPeriodAmount = 0; + this.cancellationPeriodUnits = 'hours'; + + let cancellationPeriod = this.getConfig().get('eventCancellationPeriod') || this.cancellationPeriod; + + if (!cancellationPeriod) { + return; + } + + let arr = cancellationPeriod.split(' '); + + this.cancellationPeriodAmount = parseInt(arr[0]); + this.cancellationPeriodUnits = arr[1] ?? 'hours'; }, controlAcceptanceStatusButton: function () { @@ -174,6 +196,46 @@ define('crm:views/meeting/detail', ['views/detail', 'lib!moment'], function (Dep this.removeMenuItem('sendInvitations'); }, + controlSendCancellationButton: function () { + let show = this.model.get('status') === 'Not Held'; + + if (show) { + let dateEnd = this.model.get('dateEnd'); + + if ( + dateEnd && + this.getDateTime() + .toMoment(dateEnd) + .subtract(this.cancellationPeriodAmount, this.cancellationPeriodUnits) + .isBefore(moment.now()) + ) { + show = false; + } + } + + if (show) { + let userIdList = this.model.getLinkMultipleIdList('users'); + let contactIdList = this.model.getLinkMultipleIdList('contacts'); + let leadIdList = this.model.getLinkMultipleIdList('leads'); + + if (!contactIdList.length && !leadIdList.length && !userIdList.length) { + show = false; + } + } + + if (show) { + this.addMenuItem('dropdown', { + text: this.translate('Send Cancellation', 'labels', 'Meeting'), + action: 'sendCancellation', + acl: 'edit', + }); + + return; + } + + this.removeMenuItem('sendCancellation'); + }, + actionSendInvitations: function () { Espo.Ui.notify(' ... '); @@ -188,6 +250,20 @@ define('crm:views/meeting/detail', ['views/detail', 'lib!moment'], function (Dep }); }, + actionSendCancellation: function () { + Espo.Ui.notify(' ... '); + + this.createView('dialog', 'crm:views/meeting/modals/send-cancellation', { + model: this.model, + }).then(view => { + Espo.Ui.notify(false); + + view.render(); + + this.listenToOnce(view, 'sent', () => this.model.fetch()); + }); + }, + actionSetAcceptanceStatus: function () { this.createView('dialog', 'crm:views/meeting/modals/acceptance-status', { model: this.model diff --git a/client/modules/crm/src/views/meeting/modals/send-cancellation.js b/client/modules/crm/src/views/meeting/modals/send-cancellation.js new file mode 100644 index 0000000000..057c782840 --- /dev/null +++ b/client/modules/crm/src/views/meeting/modals/send-cancellation.js @@ -0,0 +1,200 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko + * Website: https://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + * + * 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 General Public License version 3. + * + * In accordance with Section 7(b) of the GNU General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +define('crm:views/meeting/modals/send-cancellation', ['views/modal', 'collection'], function (Dep, Collection) { + /** + * @module crm_views/meeting/modals/send-cancellation + */ + + /** + * @class + * @name Class + * @extends module:views/modal.Class + * @memberOf module:crm_views/meeting/modals/send-cancellation + */ + return Dep.extend(/** @lends module:crm_views/meeting/modals/send-cancellation.Class# */{ + + backdrop: true, + + templateContent: ` +
+

{{message}}

+
+
{{{list}}}
+ `, + + data: function () { + return { + message: this.translate('sendCancellationsToSelectedAttendees', 'messages', 'Meeting'), + }; + }, + + setup: function () { + Dep.prototype.setup.call(this); + + this.shortcutKeys = {}; + this.shortcutKeys['Control+Enter'] = e => { + if (!this.hasAvailableActionItem('send')) { + return; + } + + e.preventDefault(); + + this.actionSend(); + }; + + this.$header = $('').append( + $('') + .text(this.translate(this.model.entityType, 'scopeNames')), + ' ', + $('') + .text(this.model.get('name')), + ' ', + $('') + .text(this.translate('Send Cancellation', 'labels', 'Meeting')) + ); + + this.addButton({ + label: 'Send', + name: 'send', + style: 'danger', + disabled: true, + }); + + this.addButton({ + label: 'Cancel', + name: 'cancel', + }); + + this.collection = new Collection(); + this.collection.url = this.model.entityType + `/${this.model.id}/attendees`; + + this.wait( + this.collection.fetch() + .then(() => { + Espo.Utils.clone(this.collection.models).forEach(model => { + model.entityType = model.get('_scope'); + + if (!model.get('emailAddress')) { + this.collection.remove(model.id); + } + }); + + return this.createView('list', 'views/record/list', { + selector: '.list-container', + collection: this.collection, + rowActionsDisabled: true, + massActionsDisabled: true, + checkAllResultDisabled: true, + selectable: true, + buttonsDisabled: true, + listLayout: [ + { + name: 'name', + customLabel: this.translate('name', 'fields'), + notSortable: true, + }, + { + name: 'acceptanceStatus', + width: 40, + customLabel: this.translate('acceptanceStatus', 'fields', 'Meeting'), + notSortable: true, + view: 'views/fields/enum', + params: { + options: this.model.getFieldParam('acceptanceStatus', 'options'), + style: this.model.getFieldParam('acceptanceStatus', 'style'), + }, + }, + ], + }) + }) + .then(view => { + this.collection.models + .filter(model => { + if (model.id === this.getUser().id && model.entityType === 'User') { + return false; + } + + return true; + }) + .forEach(model => { + this.getListView().checkRecord(model.id); + }); + + this.listenTo(view, 'check', () => this.controlSendButton()); + + this.controlSendButton(); + }) + ); + }, + + controlSendButton: function () { + this.getListView().checkedList.length ? + this.enableButton('send') : + this.disableButton('send'); + }, + + /** + * @return {module:views/record/list.Class} + */ + getListView: function () { + return this.getView('list'); + }, + + actionSend: function () { + this.disableButton('send'); + + Espo.Ui.notify(' ... '); + + let targets = this.getListView().checkedList.map(id => { + return { + entityType: this.collection.get(id).entityType, + id: id, + }; + }); + + Espo.Ajax + .postRequest(this.model.entityType + '/action/sendCancellation', { + id: this.model.id, + targets: targets, + }) + .then(result => { + result ? + Espo.Ui.success(this.translate('Sent')) : + Espo.Ui.warning(this.translate('nothingHasBeenSent', 'messages', 'Meeting')); + + this.trigger('sent'); + + this.close(); + }) + .catch(() => { + this.enableButton('send'); + }); + }, + }); +});