send invitations impr
This commit is contained in:
@@ -107,6 +107,7 @@ class Invitations
|
||||
'inviteeId' => $invitee->getId(),
|
||||
'inviteeType' => $invitee->getEntityType(),
|
||||
'link' => $link,
|
||||
'dateStart' => $entity->get('dateStart'),
|
||||
]);
|
||||
|
||||
if ($entity->get('dateEnd')) {
|
||||
|
||||
@@ -31,22 +31,27 @@ namespace Espo\Modules\Crm\Controllers;
|
||||
|
||||
use Espo\Core\Api\Response;
|
||||
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;
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Modules\Crm\Entities\Call as CallEntity;
|
||||
use Espo\Modules\Crm\Tools\Meeting\InvitationService;
|
||||
use Espo\Modules\Crm\Tools\Meeting\Invitee;
|
||||
use Espo\Modules\Crm\Tools\Meeting\Service;
|
||||
use stdClass;
|
||||
|
||||
class Call extends Record
|
||||
{
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws ForbiddenSilent
|
||||
* @throws Error
|
||||
* @throws SendingError
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function postActionSendInvitations(Request $request): bool
|
||||
@@ -57,13 +62,52 @@ class Call extends Record
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$invitees = $this->fetchInvitees($request);
|
||||
|
||||
$resultList = $this->injectableFactory
|
||||
->create(InvitationService::class)
|
||||
->send(CallEntity::ENTITY_TYPE, $id);
|
||||
->send(CallEntity::ENTITY_TYPE, $id, $invitees);
|
||||
|
||||
return $resultList !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return ?Invitee[]
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function fetchInvitees(Request $request): ?array
|
||||
{
|
||||
$targets = $request->getParsedBody()->targets ?? null;
|
||||
|
||||
if ($targets === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_array($targets)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$invitees = [];
|
||||
|
||||
foreach ($targets as $target) {
|
||||
if (!$target instanceof stdClass) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$targetEntityType = $target->entityType ?? null;
|
||||
$targetId = $target->id ?? null;
|
||||
|
||||
if (!is_string($targetEntityType) || !is_string($targetId)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$invitees[] = new Invitee($targetEntityType, $targetId);
|
||||
}
|
||||
|
||||
return $invitees;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Espo\Modules\Crm\Controllers;
|
||||
|
||||
use Espo\Core\Api\Response;
|
||||
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;
|
||||
@@ -38,17 +39,21 @@ use Espo\Core\Exceptions\NotFound;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
|
||||
use Espo\Core\Mail\Exceptions\SendingError;
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Modules\Crm\Entities\Meeting as MeetingEntity;
|
||||
use Espo\Modules\Crm\Tools\Meeting\InvitationService;
|
||||
use Espo\Modules\Crm\Tools\Meeting\Invitee;
|
||||
use Espo\Modules\Crm\Tools\Meeting\Service;
|
||||
use stdClass;
|
||||
|
||||
class Meeting extends Record
|
||||
{
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws ForbiddenSilent
|
||||
* @throws Error
|
||||
* @throws SendingError
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function postActionSendInvitations(Request $request): bool
|
||||
@@ -59,13 +64,52 @@ class Meeting extends Record
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$invitees = $this->fetchInvitees($request);
|
||||
|
||||
$resultList = $this->injectableFactory
|
||||
->create(InvitationService::class)
|
||||
->send(MeetingEntity::ENTITY_TYPE, $id);
|
||||
->send(MeetingEntity::ENTITY_TYPE, $id, $invitees);
|
||||
|
||||
return $resultList !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return ?Invitee[]
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function fetchInvitees(Request $request): ?array
|
||||
{
|
||||
$targets = $request->getParsedBody()->targets ?? null;
|
||||
|
||||
if ($targets === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_array($targets)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$invitees = [];
|
||||
|
||||
foreach ($targets as $target) {
|
||||
if (!$target instanceof stdClass) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$targetEntityType = $target->entityType ?? null;
|
||||
$targetId = $target->id ?? null;
|
||||
|
||||
if (!is_string($targetEntityType) || !is_string($targetId)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$invitees[] = new Invitee($targetEntityType, $targetId);
|
||||
}
|
||||
|
||||
return $invitees;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
|
||||
@@ -34,6 +34,7 @@ use Espo\Core\Api\Response;
|
||||
use Espo\Core\EntryPoint\EntryPoint;
|
||||
use Espo\Core\EntryPoint\Traits\NoAuth;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\HookManager;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
@@ -67,6 +68,7 @@ class EventConfirmation implements EntryPoint
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
public function run(Request $request, Response $response): void
|
||||
{
|
||||
@@ -100,6 +102,7 @@ class EventConfirmation implements EntryPoint
|
||||
$inviteeType = $data->inviteeType ?? null;
|
||||
$inviteeId = $data->inviteeId ?? null;
|
||||
$link = $data->link ?? null;
|
||||
$sentDateStart = $data->dateStart ?? null;
|
||||
|
||||
$toProcess =
|
||||
$eventType &&
|
||||
@@ -109,7 +112,11 @@ class EventConfirmation implements EntryPoint
|
||||
$link;
|
||||
|
||||
if (!$toProcess) {
|
||||
throw new BadRequest();
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if ($sentDateStart !== null && !is_string($sentDateStart)) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$event = $this->entityManager->getEntityById($eventType, $eventId);
|
||||
@@ -187,6 +194,8 @@ class EventConfirmation implements EntryPoint
|
||||
$actionData['translatedStatus'] = $this->language->translateOption($status, 'acceptanceStatus', $eventType);
|
||||
$actionData['style'] = $this->metadata
|
||||
->get(['entityDefs', $eventType, 'fields', 'acceptanceStatus', 'style', $status]);
|
||||
$actionData['sentDateStart'] = $sentDateStart;
|
||||
$actionData['statusTranslation'] = $this->language->get([Meeting::ENTITY_TYPE, 'options', 'acceptanceStatus']);
|
||||
|
||||
$this->actionRenderer->write(
|
||||
$response,
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
"todays": "Today's"
|
||||
},
|
||||
"messages": {
|
||||
"sendInvitationsConfirmation": "Send invitation emails to attendees?",
|
||||
"sendInvitationsToSelectedAttendees": "Invitation emails will be sent to the selected attendees.",
|
||||
"selectAcceptanceStatus": "Set your acceptance status.",
|
||||
"nothingHasBeenSent": "Nothing were sent"
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<p>
|
||||
<a href="{{acceptLink}}">Annehmen</a>, <a href="{{declineLink}}">Decline</a>, <a href="{{tentativeLink}}">mit Vorbehalt</a>
|
||||
<a href="{{acceptLink}}" style="font-size: 1.2em">Annehmen</a> ·
|
||||
<a href="{{tentativeLink}}" style="font-size: 1.2em">mit Vorbehalt</a> ·
|
||||
<a href="{{declineLink}}" style="font-size: 1.2em">Decline</a>
|
||||
</p>
|
||||
{{#if isUser}}
|
||||
<p><a href="{{recordUrl}}">Eintrag öffnen</a></p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<p>
|
||||
<a href="{{acceptLink}}">Accept</a>, <a href="{{declineLink}}">Decline</a>, <a href="{{tentativeLink}}">Tentative</a>
|
||||
<a href="{{acceptLink}}" style="font-size: 1.2em">Accept</a> ·
|
||||
<a href="{{tentativeLink}}" style="font-size: 1.2em">Tentative</a> ·
|
||||
<a href="{{declineLink}}" style="font-size: 1.2em">Decline</a>
|
||||
</p>
|
||||
{{#if isUser}}
|
||||
<p><a href="{{recordUrl}}">View record</a></p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<p>
|
||||
<a href="{{acceptLink}}">Aceptar</a>, <a href="{{declineLink}}">Declinar</a>, <a href="{{tentativeLink}}">Provisional</a>
|
||||
<a href="{{acceptLink}}" style="font-size: 1.2em">Aceptar</a> ·
|
||||
<a href="{{tentativeLink}}" style="font-size: 1.2em">Provisional</a> ·
|
||||
<a href="{{declineLink}}" style="font-size: 1.2em">Declinar</a>
|
||||
</p>
|
||||
{{#if isUser}}
|
||||
<p><a href="{{recordUrl}}">Ver registro</a></p>
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<p>
|
||||
<a href="{{acceptLink}}">Accepter</a>, <a href="{{declineLink}}">Décliner</a>, <a href="{{tentativeLink}}">Tentative</a>
|
||||
<a href="{{acceptLink}}" style="font-size: 1.2em">Accepter</a> ·
|
||||
<a href="{{tentativeLink}}" style="font-size: 1.2em">Tentative</a> ·
|
||||
<a href="{{declineLink}}" style="font-size: 1.2em">Décliner</a>
|
||||
</p>
|
||||
{{#if isUser}}
|
||||
<p><a href="{{recordUrl}}">Voir la fiche</a></p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<p>
|
||||
<a href="{{acceptLink}}">Прийняти</a>, <a href="{{declineLink}}">Відхилити</a>, <a href="{{tentativeLink}}">Не впевнений</a>
|
||||
<a href="{{acceptLink}}" style="font-size: 1.2em">Прийняти</a> ·
|
||||
<a href="{{tentativeLink}}" style="font-size: 1.2em">Не впевнений</a> ·
|
||||
<a href="{{declineLink}}" style="font-size: 1.2em">Відхилити</a>
|
||||
</p>
|
||||
{{#if isUser}}
|
||||
<p><a href="{{recordUrl}}">Відкрити запис</a></p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
@@ -41,10 +41,7 @@ use Espo\Core\Record\ServiceContainer as RecordServiceContainer;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Crm\Business\Event\Invitations;
|
||||
use Espo\Modules\Crm\Entities\Contact;
|
||||
use Espo\Modules\Crm\Entities\Lead;
|
||||
use Espo\Modules\Crm\Entities\Meeting;
|
||||
use Espo\ORM\Collection;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Tools\Email\SendService;
|
||||
@@ -81,13 +78,14 @@ class InvitationService
|
||||
/**
|
||||
* Send invitations for a meeting (or call). Checks access. Uses user's SMTP if available.
|
||||
*
|
||||
* @param ?Invitee[] $targets
|
||||
* @return Entity[] Entities an invitation was sent to.
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws SendingError
|
||||
*/
|
||||
public function send(string $entityType, string $id): array
|
||||
public function send(string $entityType, string $id, ?array $targets = null): array
|
||||
{
|
||||
$entity = $this->recordServiceContainer
|
||||
->get($entityType)
|
||||
@@ -101,53 +99,50 @@ class InvitationService
|
||||
throw new Forbidden("No edit access.");
|
||||
}
|
||||
|
||||
$linkList = [
|
||||
'users',
|
||||
'contacts',
|
||||
'leads',
|
||||
];
|
||||
|
||||
$sender = $this->getSender();
|
||||
|
||||
$sentAddressList = [];
|
||||
$resultEntityList = [];
|
||||
|
||||
foreach ($this->getUsers($entity) as $user) {
|
||||
$emailAddress = $user->getEmailAddress();
|
||||
foreach ($linkList as $link) {
|
||||
$builder = $this->entityManager
|
||||
->getRDBRepository($entityType)
|
||||
->getRelation($entity, $link);
|
||||
|
||||
if ($emailAddress) {
|
||||
$sender->sendInvitation($entity, $user, 'users');
|
||||
|
||||
$sentAddressList[] = $emailAddress;
|
||||
$resultEntityList[] = $user;
|
||||
if ($targets === null) {
|
||||
$builder->where([
|
||||
'@relation.status=' => Meeting::ATTENDEE_STATUS_NONE,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var Collection<Contact> $contacts */
|
||||
$contacts = $this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, 'contacts')
|
||||
->find();
|
||||
$collection = $builder->find();
|
||||
|
||||
/** @var Collection<Lead> $leads */
|
||||
$leads = $this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, 'leads')
|
||||
->find();
|
||||
foreach ($collection as $attendee) {
|
||||
if ($targets && !self::isInTargets($attendee, $targets)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($contacts as $contact) {
|
||||
$emailAddress = $contact->getEmailAddress();
|
||||
$emailAddress = $attendee->get('emailAddress');
|
||||
|
||||
if ($emailAddress && !in_array($emailAddress, $sentAddressList)) {
|
||||
$sender->sendInvitation($entity, $contact, 'contacts');
|
||||
if (!$emailAddress || in_array($emailAddress, $sentAddressList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sender->sendInvitation($entity, $attendee, $link);
|
||||
|
||||
$sentAddressList[] = $emailAddress;
|
||||
$resultEntityList[] = $contact;
|
||||
}
|
||||
}
|
||||
$resultEntityList[] = $attendee;
|
||||
|
||||
foreach ($leads as $lead) {
|
||||
$emailAddress = $lead->getEmailAddress();
|
||||
|
||||
if ($emailAddress && !in_array($emailAddress, $sentAddressList)) {
|
||||
$sender->sendInvitation($entity, $lead, 'leads');
|
||||
|
||||
$sentAddressList[] = $emailAddress;
|
||||
$resultEntityList[] = $lead;
|
||||
$this->entityManager
|
||||
->getRDBRepository($entityType)
|
||||
->getRelation($entity, $link)
|
||||
->updateColumns($attendee, ['status' => Meeting::ATTENDEE_STATUS_NONE]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,26 +150,20 @@ class InvitationService
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<User>
|
||||
* @param Invitee[] $targets
|
||||
*/
|
||||
private function getUsers(Entity $entity): Collection
|
||||
private static function isInTargets(Entity $entity, array $targets): bool
|
||||
{
|
||||
/** @var Collection<User> */
|
||||
return $this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, 'users')
|
||||
->where([
|
||||
'OR' => [
|
||||
[
|
||||
'id=' => $this->user->getId(),
|
||||
'@relation.status!=' => Meeting::ATTENDEE_STATUS_ACCEPTED,
|
||||
],
|
||||
[
|
||||
'id!=' => $this->user->getId(),
|
||||
]
|
||||
]
|
||||
])
|
||||
->find();
|
||||
foreach ($targets as $target) {
|
||||
if (
|
||||
$entity->getEntityType() === $target->getEntityType() &&
|
||||
$entity->getId() === $target->getId()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getSender(): Invitations
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Espo\Modules\Crm\Tools\Meeting;
|
||||
|
||||
class Invitee
|
||||
{
|
||||
public function __construct(private string $entityType, private string $id)
|
||||
{}
|
||||
|
||||
public function getEntityType(): string
|
||||
{
|
||||
return $this->entityType;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,30 @@
|
||||
<div class="block-center-md">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<h4>{{actionData.translatedEntityType}}: {{actionData.eventName}}</h4>
|
||||
<p class="margin-top-2x">
|
||||
<h4 class="margin-bottom-2x">{{actionData.translatedEntityType}}: {{actionData.eventName}}</h4>
|
||||
<div class="margin-bottom-2x">
|
||||
{{#if dateStartChanged}}
|
||||
<div style="text-decoration: line-through;">{{sentDateStart}}</div>
|
||||
{{/if}}
|
||||
<div>{{dateStart}}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label label-{{style}} label-md">{{actionData.translatedStatus}}</span>
|
||||
</p>
|
||||
<div class="btn-group">
|
||||
<a role="button" class="dropdown-toggle text-soft" data-toggle="dropdown">
|
||||
<span class="fas fa-ellipsis-h"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
{{#each actionDataList}}
|
||||
<li>
|
||||
<a {{#if link}}href="{{link}}"{{/if}}>{{label}}
|
||||
{{#if active}}<span class="fas fa-check pull-right"></span>{{/if}}
|
||||
</a>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,19 +26,61 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
define('crm:views/event-confirmation/confirmation', ['view'], function (Dep) {
|
||||
define('crm:views/event-confirmation/confirmation', ['view', 'lib!moment'], function (Dep, moment) {
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
template: 'crm:event-confirmation/confirmation',
|
||||
|
||||
data: function () {
|
||||
let style = this.options.actionData.style || 'default';
|
||||
let style = this.actionData.style || 'default';
|
||||
|
||||
return {
|
||||
actionData: this.options.actionData,
|
||||
actionData: this.actionData,
|
||||
style: style,
|
||||
dateStart: this.actionData.dateStart ?
|
||||
this.convertDateTime(this.actionData.dateStart) : null,
|
||||
sentDateStart: this.actionData.sentDateStart ?
|
||||
this.convertDateTime(this.actionData.sentDateStart) : null,
|
||||
dateStartChanged: this.actionData.sentDateStart &&
|
||||
this.actionData.dateStart !== this.actionData.sentDateStart,
|
||||
actionDataList: this.getActionDataList(),
|
||||
};
|
||||
},
|
||||
|
||||
setup: function () {
|
||||
this.actionData = this.options.actionData;
|
||||
},
|
||||
|
||||
getActionDataList: function () {
|
||||
let actionMap = {
|
||||
'Accepted': 'accept',
|
||||
'Declined': 'decline',
|
||||
'Tentative': 'tentative',
|
||||
};
|
||||
|
||||
let statusList = ['Accepted', 'Tentative', 'Declined'];
|
||||
|
||||
let url = window.location.href.replace('action=' + actionMap[this.actionData.status], 'action={action}');
|
||||
|
||||
return statusList.map(item => {
|
||||
let active = item === this.actionData.status;
|
||||
return {
|
||||
active: active,
|
||||
link: active ? '' : url.replace('{action}', actionMap[item]),
|
||||
label: this.actionData.statusTranslation[item],
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
convertDateTime: function (value) {
|
||||
let timezone = this.getConfig().get('timeZone');
|
||||
|
||||
let m = this.getDateTime().toMoment(value)
|
||||
.tz(timezone);
|
||||
|
||||
return m.format(this.getDateTime().getDateTimeFormat()) + ' ' +
|
||||
m.format('Z z');
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -128,7 +128,7 @@ define('crm:views/meeting/detail', ['views/detail', 'lib!moment'], function (Dep
|
||||
if (!contactIdList.length && !leadIdList.length && !userIdList.length) {
|
||||
show = false;
|
||||
}
|
||||
else if (
|
||||
/*else if (
|
||||
!contactIdList.length &&
|
||||
!leadIdList.length &&
|
||||
userIdList.length === 1 &&
|
||||
@@ -136,6 +136,17 @@ define('crm:views/meeting/detail', ['views/detail', 'lib!moment'], function (Dep
|
||||
this.model.getLinkMultipleColumn('users', 'status', this.getUser().id) === 'Accepted'
|
||||
) {
|
||||
show = false;
|
||||
}*/
|
||||
}
|
||||
|
||||
if (show) {
|
||||
let dateEnd = this.model.get('dateEnd');
|
||||
|
||||
if (
|
||||
dateEnd &&
|
||||
this.getDateTime().toMoment(dateEnd).isBefore(moment.now())
|
||||
) {
|
||||
show = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,30 +175,17 @@ define('crm:views/meeting/detail', ['views/detail', 'lib!moment'], function (Dep
|
||||
},
|
||||
|
||||
actionSendInvitations: function () {
|
||||
this.confirm({
|
||||
message: this.translate('sendInvitationsConfirmation', 'messages', 'Meeting'),
|
||||
})
|
||||
.then(() => {
|
||||
this.disableMenuItem('sendInvitations');
|
||||
this.notify('Sending...');
|
||||
Espo.Ui.notify(' ... ');
|
||||
|
||||
Espo.Ajax
|
||||
.postRequest(this.model.entityType + '/action/sendInvitations', {
|
||||
id: this.model.id,
|
||||
})
|
||||
.then(result => {
|
||||
if (result) {
|
||||
this.notify('Sent', 'success');
|
||||
} else {
|
||||
Espo.Ui.warning(this.translate('nothingHasBeenSent', 'messages', 'Meeting'));
|
||||
}
|
||||
this.createView('dialog', 'crm:views/meeting/modals/send-invitations', {
|
||||
model: this.model,
|
||||
}).then(view => {
|
||||
Espo.Ui.notify(false);
|
||||
|
||||
this.enableMenuItem('sendInvitations');
|
||||
})
|
||||
.catch(() => {
|
||||
this.enableMenuItem('sendInvitations');
|
||||
});
|
||||
});
|
||||
view.render();
|
||||
|
||||
this.listenToOnce(view, 'sent', () => this.model.fetch());
|
||||
});
|
||||
},
|
||||
|
||||
actionSetAcceptanceStatus: function () {
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
/************************************************************************
|
||||
* 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-invitations', ['views/modal', 'collection'], function (Dep, Collection) {
|
||||
/**
|
||||
* @module crm_views/meeting/modals/send-invitations
|
||||
*/
|
||||
|
||||
/**
|
||||
* @class
|
||||
* @name Class
|
||||
* @extends module:views/modal.Class
|
||||
* @memberOf module:crm_views/meeting/modals/send-invitations
|
||||
*/
|
||||
return Dep.extend(/** @lends module:crm_views/meeting/modals/send-invitations.Class# */{
|
||||
|
||||
backdrop: true,
|
||||
|
||||
templateContent: `
|
||||
<div class="margin-bottom">
|
||||
<p>{{message}}</p>
|
||||
</div>
|
||||
<div class="list-container">{{{list}}}</div>
|
||||
`,
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
message: this.translate('sendInvitationsToSelectedAttendees', '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 Invitations', '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 => {
|
||||
let status = model.get('acceptanceStatus');
|
||||
|
||||
return !status || status === 'None';
|
||||
})
|
||||
.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/sendInvitations', {
|
||||
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');
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -26,7 +26,7 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
define('collection', [], function () {
|
||||
define('collection', ['model'], function (Model) {
|
||||
|
||||
/**
|
||||
* On sync with backend.
|
||||
@@ -204,6 +204,8 @@ define('collection', [], function () {
|
||||
this.defaultOrderBy = this.orderBy;
|
||||
|
||||
this.data = {};
|
||||
|
||||
this.model = options.model || Model;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,7 +39,10 @@ define('multi-collection', ['collection'], function (Collection) {
|
||||
return Collection.extend(/** @lends module:multi-collection.Class# */{
|
||||
|
||||
/**
|
||||
* @private
|
||||
* A model seed map.
|
||||
*
|
||||
* @public
|
||||
* @type {Object.<string, module:model.Class>}
|
||||
*/
|
||||
seeds: null,
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ function (Dep, /** module:ui/multi-select*/MultiSelect, /** module:ui/select*/Se
|
||||
);
|
||||
}
|
||||
|
||||
this.styleMap = this.model.getFieldParam(this.name, 'style') || {};
|
||||
this.styleMap = this.params.style || this.model.getFieldParam(this.name, 'style') || {};
|
||||
|
||||
this.setupOptions();
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) {
|
||||
/**
|
||||
* A scope. Set automatically.
|
||||
*
|
||||
* @type {string|null}
|
||||
* @type {?string}
|
||||
*/
|
||||
scope: null,
|
||||
|
||||
@@ -1913,13 +1913,13 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) {
|
||||
|
||||
this.checkAllResultMassActionList = checkAllResultMassActionList;
|
||||
|
||||
let metadataCheckkAllMassActionList = (
|
||||
let metadataCheckAllMassActionList = (
|
||||
this.getMetadata().get(['clientDefs', this.scope, 'checkAllResultMassActionList']) || []
|
||||
).concat(
|
||||
this.getMetadata().get(['clientDefs', 'Global', 'checkAllResultMassActionList']) || []
|
||||
);
|
||||
|
||||
metadataCheckkAllMassActionList.forEach(item => {
|
||||
metadataCheckAllMassActionList.forEach(item => {
|
||||
if (this.collection.url !== this.entityType) {
|
||||
return;
|
||||
}
|
||||
@@ -1941,7 +1941,7 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) {
|
||||
});
|
||||
|
||||
metadataMassActionList
|
||||
.concat(metadataCheckkAllMassActionList)
|
||||
.concat(metadataCheckAllMassActionList)
|
||||
.forEach(action => {
|
||||
let defs = this.getMetadata()
|
||||
.get(['clientDefs', this.scope, 'massActionDefs', action]) || {};
|
||||
@@ -2216,7 +2216,9 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) {
|
||||
_loadListLayout: function (callback) {
|
||||
this.layoutLoadCallbackList.push(callback);
|
||||
|
||||
if (this.layoutIsBeingLoaded) return;
|
||||
if (this.layoutIsBeingLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.layoutIsBeingLoaded = true;
|
||||
|
||||
@@ -2257,7 +2259,7 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._loadListLayout((listLayout) => {
|
||||
this._loadListLayout(listLayout => {
|
||||
this.listLayout = listLayout;
|
||||
|
||||
let attributeList = this.fetchAttributeListFromLayout();
|
||||
@@ -2433,6 +2435,16 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) {
|
||||
item.options.defs.align = col.align;
|
||||
}
|
||||
|
||||
if (col.options) {
|
||||
for (let optionName in col.options) {
|
||||
if (typeof item.options[optionName] !== 'undefined') {
|
||||
continue;
|
||||
}
|
||||
|
||||
item.options[optionName] = col.options[optionName];
|
||||
}
|
||||
}
|
||||
|
||||
layout.push(item);
|
||||
}
|
||||
if (this.rowActionsView && !this.rowActionsDisabled) {
|
||||
@@ -2452,20 +2464,17 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) {
|
||||
checkRecord: function (id, $target, isSilent) {
|
||||
$target = $target || this.$el.find('.record-checkbox[data-id="' + id + '"]');
|
||||
|
||||
if (!$target.length) {
|
||||
return;
|
||||
if ($target.length) {
|
||||
$target.get(0).checked = true;
|
||||
$target.closest('tr').addClass('active');
|
||||
}
|
||||
|
||||
$target.get(0).checked = true;
|
||||
|
||||
var index = this.checkedList.indexOf(id);
|
||||
let index = this.checkedList.indexOf(id);
|
||||
|
||||
if (index === -1) {
|
||||
this.checkedList.push(id);
|
||||
}
|
||||
|
||||
$target.closest('tr').addClass('active');
|
||||
|
||||
this.handleAfterCheck(isSilent);
|
||||
},
|
||||
|
||||
@@ -2481,18 +2490,15 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) {
|
||||
|
||||
if ($target.get(0)) {
|
||||
$target.get(0).checked = false;
|
||||
$target.closest('tr').removeClass('active');
|
||||
}
|
||||
|
||||
var index = this.checkedList.indexOf(id);
|
||||
let index = this.checkedList.indexOf(id);
|
||||
|
||||
if (index !== -1) {
|
||||
this.checkedList.splice(index, 1);
|
||||
}
|
||||
|
||||
if ($target.get(0)) {
|
||||
$target.closest('tr').removeClass('active');
|
||||
}
|
||||
|
||||
this.handleAfterCheck(isSilent);
|
||||
},
|
||||
|
||||
@@ -2587,17 +2593,19 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) {
|
||||
* @protected
|
||||
*/
|
||||
getInternalLayout: function (callback, model) {
|
||||
if (this.scope === null || this.rowHasOwnLayout) {
|
||||
if (
|
||||
(this.scope === null || this.rowHasOwnLayout) &&
|
||||
!Array.isArray(this.listLayout)
|
||||
) {
|
||||
if (!model) {
|
||||
callback(null);
|
||||
|
||||
return;
|
||||
}
|
||||
else {
|
||||
this.getInternalLayoutForModel(callback, model);
|
||||
|
||||
return;
|
||||
}
|
||||
this.getInternalLayoutForModel(callback, model);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._internalLayout !== null) {
|
||||
@@ -2614,7 +2622,7 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._loadListLayout((listLayout) => {
|
||||
this._loadListLayout(listLayout => {
|
||||
this.listLayout = listLayout;
|
||||
this._internalLayout = this._convertLayout(listLayout);
|
||||
|
||||
@@ -2652,7 +2660,7 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) {
|
||||
|
||||
this.rowList.push(key);
|
||||
|
||||
this.getInternalLayout((internalLayout) => {
|
||||
this.getInternalLayout(internalLayout => {
|
||||
internalLayout = Espo.Utils.cloneDeep(internalLayout);
|
||||
|
||||
this.prepareInternalLayout(internalLayout, model);
|
||||
|
||||
@@ -224,6 +224,20 @@ a.text-muted {
|
||||
}
|
||||
}
|
||||
|
||||
a.text-soft {
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--gray-soft);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
> .fas,
|
||||
> .far {
|
||||
color: var(--gray-soft);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
.text-emphasis-variant-espo(var(--state-primary-text), var(--brand-primary-10));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user