cancellation email
This commit is contained in:
@@ -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<string,string|int|null> $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";
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
},
|
||||
"templates": {
|
||||
"invitation": "Invitation",
|
||||
"cancellation": "Cancellation",
|
||||
"reminder": "Reminder"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
"scopeList": ["Meeting", "Call"],
|
||||
"module": "Crm"
|
||||
},
|
||||
"cancellation": {
|
||||
"scopeList": ["Meeting", "Call"],
|
||||
"module": "Crm"
|
||||
},
|
||||
"reminder": {
|
||||
"scopeList": ["Meeting", "Call", "Task"],
|
||||
"module": "Crm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<p>Subject: {{name}}</p>
|
||||
<p>Start: {{#if isAllDay}}{{dateStartDate}}{{else}}{{dateStart}}{{/if}}</p>
|
||||
{{#if isUser}}
|
||||
<p><a href="{{recordUrl}}">View record</a></p>
|
||||
{{/if}}
|
||||
@@ -0,0 +1 @@
|
||||
Cancellation of {{entityTypeLowerFirst}} '{{name}}'
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: `
|
||||
<div class="margin-bottom">
|
||||
<p>{{message}}</p>
|
||||
</div>
|
||||
<div class="list-container">{{{list}}}</div>
|
||||
`,
|
||||
|
||||
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 = $('<span>').append(
|
||||
$('<span>')
|
||||
.text(this.translate(this.model.entityType, 'scopeNames')),
|
||||
' <span class="chevron-right"></span> ',
|
||||
$('<span>')
|
||||
.text(this.model.get('name')),
|
||||
' <span class="chevron-right"></span> ',
|
||||
$('<span>')
|
||||
.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');
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user