Announcement

Collapse
No announcement yet.

Participant functionality in a custom entity.

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

  • espcrm
    commented on 's reply
    Nevermind: Speak of the devil: https://github.com/espocrm/espocrm/issues/1642

    I think if I update to 5.8.4 it should work.

    UPDATE: Yes worked, it showed for me now. Can send it to Contacts.
    Last edited by espcrm; 03-18-2020, 07:31 AM.

  • espcrm
    commented on 's reply
    Nevermind: Speak of the devil: https://github.com/espocrm/espocrm/issues/1642

    I think if I update to 5.8.4 it should work.

  • espcrm
    replied
    Maximus Weird, I don't have that option to send Invitation.

    I tested the demo and does show it on there and the Send Invitation did work.

    Is there a setting we need to enable because it not showing for me. Also I noticed that the "send invitation" don't create an email History/Archive. Is that normal?

    Leave a comment:


  • Maximus
    commented on 's reply
    You can send an invitation to the users (any type), contacts and leads as well.

  • espcrm
    replied
    Originally posted by Maximus View Post
    Hello,
    espcrm
    > Can you send an invite if they are an portal user?
    Yes, you can. Just change a filter from 'Active' to 'All' upon adding a new user to the event to be able to select a portal user.
    So they must be a portal user? They can't just be a Contact/Account?

    Leave a comment:


  • telecastg
    replied
    Thanks Maximus can you tell me which scripts I should explore ?

    Leave a comment:


  • Maximus
    replied
    Hello,
    espcrm
    > Can you send an invite if they are an portal user?
    Yes, you can. Just change a filter from 'Active' to 'All' upon adding a new user to the event to be able to select a portal user.

    telecastg
    We do not have documentation of how it is translated into SQL. This conversion logic is buried deep in the code. I can only suggest you use grep to explore it.

    Leave a comment:


  • espcrm
    commented on 's reply
    Tested the Email button and it work good. Don't need to log in to Accept/Decline/Tentative. As long as you click the button the meeting Status will change.

    EDIT: Found out where you can email the email template:

    #Admin/templateManager/
    Last edited by espcrm; 03-12-2020, 02:22 AM.

  • espcrm
    replied
    Wow, I didn't even notice there is a "Send Invitations" button until I recently noticed it and tested it.

    But this is only usable for "Users?" ie. Staff members? You can't seem to send invite to Contact(s)? Can you send an invite if they are an portal user? The Demo I think disable email system so bit hard to test.

    The Email Template also look rather ugly and plain, need way to customize it too, anyway we can?


    Email looks:
    Click image for larger version

Name:	Meeting Invitation 2 - email.png
Views:	504
Size:	6.6 KB
ID:	56716

    Meeting layout (top right for Send Invitation)
    Click image for larger version

Name:	Meeting Invitation.png
Views:	643
Size:	67.2 KB
ID:	56715
    Last edited by espcrm; 03-12-2020, 02:17 AM.

    Leave a comment:


  • telecastg
    replied
    Thank you so much for the detailed instructions !

    I have been trying to find documentation on the use of the "where" syntax applied above, in order to understand how is exactly this JSON structure translated into an actual sql statement. Can you point out where I could find more information ?

    PHP Code:

    "where": {

      
    "=": {
        
    "leftJoins": ["users""contacts""leads"],
        
    "sql""contactsMiddle.status = {value} OR leadsMiddle.status = {value} OR usersMiddle.status = {value}",
        
    "distinct"true
      
    },

      
    "<>""event.id NOT IN (SELECT event_id FROM contact_event WHERE deleted = 0 AND status = {value}) AND event.id NOT IN (SELECT event_id FROM event_user WHERE deleted = 0 AND status = {value}) AND event.id NOT IN (SELECT event_id FROM event_lead WHERE deleted = 0 AND status = {value})",

      
    "IN": {
        
    "leftJoins": ["users""leads""contacts"],
        
    "sql""contactsMiddle.status IN {value} OR leadsMiddle.status IN {value} OR usersMiddle.status IN {value}",
        
    "distinct"true
      
    },

      
    "NOT IN": {
        
    "event.id NOT IN (SELECT event_id FROM contact_event WHERE deleted = 0 AND status IN {value}) AND event.id NOT IN (SELECT event_id FROM event_user WHERE deleted = 0 AND status IN {value}) AND event.id NOT IN (SELECT event_id FROM event_lead WHERE deleted = 0 AND status IN {value})",
      },

      
    "IS NULL": {  
        
    "leftJoins": ["users""contacts""leads"],
        
    "sql""contactsMiddle.status IS NULL AND leadsMiddle.status IS NULL AND usersMiddle.status IS NULL",
        
    "distinct"true
      
    },

      
    "IS NOT NULL""event.id NOT IN (SELECT event_id FROM contact_event WHERE deleted = 0 AND status IS NULL) OR event.id NOT IN (SELECT event_id FROM event_user WHERE deleted = 0 AND status IS NULL) OR event.id NOT IN (SELECT event_id FROM event_lead WHERE deleted = 0 AND status IS NULL)"


    Thanks in advance.

    Leave a comment:


  • Maximus
    replied
    Part 2

    5. Add to the file /custom/Espo/Custom/Resources/metadata/entityDefs/User.json these fields and link:

    Code:
    {
        "fields": {
            "acceptanceStatus": {
                "type": "varchar",
                "notStorable": true,
                "exportDisabled": true,
                "disabled": true
            },
            "acceptanceStatusEvents": {
                "type": "enum",
                "notStorable": true,
                "directUpdateDisabled": true,
                "layoutAvailabilityList": ["filters"],
                "importDisabled": true,
                "exportDisabled": true,
                "view": "crm:views/lead/fields/acceptance-status",
                "link": "events",
                "column": "status"
            }
        },
        "links": {
            "events": {
                "type": "hasMany",
                "entity": "Event",
                "foreign": "users"
            }
        }
    }
    6. Add the next code into the file /custom/Espo/Custom/Controllers/Event.php:

    PHP Code:
    <?php

    namespace Espo\Custom\Controllers;

    use 
    \Espo\Core\Exceptions\Forbidden;
    use 
    \Espo\Core\Exceptions\BadRequest;
    use 
    \Espo\Core\Exceptions\NotFound;

    class 
    Event extends \Espo\Core\Templates\Controllers\Event
    {

        public function 
    postActionSendInvitations($params$data)
        {
            if (empty(
    $data->id)) {
                throw new 
    BadRequest();
            }

            
    $entity $this->getRecordService()->getEntity($data->id);

            if (!
    $entity) {
                throw new 
    NotFound();
            }

            if (!
    $this->getAcl()->check($entity'edit')) {
                throw new 
    Forbidden();
            }

            if (!
    $this->getAcl()->checkScope('Email''create')) {
                throw new 
    Forbidden();
            }

            return 
    $this->getRecordService()->sendInvitations($entity);
        }

        public function 
    postActionSetAcceptanceStatus($params$data)
        {
            if (empty(
    $data->id) || empty($data->status)) {
                throw new 
    BadRequest();
            }

            return 
    $this->getRecordService()->setAcceptanceStatus($data->id$data->status);
        }
    }
    7. Add the next code into the file /custom/Espo/Custom/Services/Event.php:

    PHP Code:
    <?php

    namespace Espo\Custom\Services;

    use 
    \Espo\ORM\Entity;
    use 
    \Espo\Modules\Crm\Business\Event\Invitations;

    use 
    \Espo\Core\Exceptions\Error;
    use 
    \Espo\Core\Exceptions\Forbidden;
    use 
    \Espo\Core\Exceptions\BadRequest;

    class 
    Event extends \Espo\Core\Templates\Services\Event
    {

        protected function 
    init()
        {
            
    $this->addDependencyList([
                
    'preferences',
                
    'language',
                
    'dateTime',
                
    'container',
                
    'fileManager',
                
    'number'
            
    ]);
        }

        protected 
    $duplicateIgnoreAttributeList = ['usersColumns''contactsColumns''leadsColumns'];

        protected function 
    getMailSender()
        {
            return 
    $this->getInjection('container')->get('mailSender');
        }

        protected function 
    getPreferences()
        {
            return 
    $this->getInjection('preferences');
        }

        protected function 
    getLanguage()
        {
            return 
    $this->getInjection('language');
        }

        protected function 
    getDateTime()
        {
            return 
    $this->getInjection('dateTime');
        }

        public function 
    checkAssignment(Entity $entity)
        {
            
    $result parent::checkAssignment($entity);
            if (!
    $result) return false;

            
    $userIdList $entity->get('usersIds');
            if (!
    is_array($userIdList)) {
                
    $userIdList = [];
            }

            
    $newIdList = [];
            if (!
    $entity->isNew()) {
                
    $existingIdList = [];
                foreach (
    $entity->get('users') as $user) {
                    
    $existingIdList[] = $user->id;
                }
                foreach (
    $userIdList as $id) {
                    if (!
    in_array($id$existingIdList)) {
                        
    $newIdList[] = $id;
                    }
                }
            } else {
                
    $newIdList $userIdList;
            }

            foreach (
    $newIdList as $userId) {
                if (!
    $this->getAcl()->checkAssignmentPermission($userId)) {
                    return 
    false;
                }
            }

            return 
    true;
        }

        protected function 
    getInvitationManager($useUserSmtp true)
        {
            
    $smtpParams null;
            if (
    $useUserSmtp) {
                
    $smtpParams $this->getServiceFactory()->create('Email')->getUserSmtpParams($this->getUser()->id);
            }

            
    $templateFileManager $this->getInjection('container')->get('templateFileManager');

            return new 
    Invitations(
                
    $this->getEntityManager(),
                
    $smtpParams,
                
    $this->getMailSender(),
                
    $this->getConfig(),
                
    $this->getInjection('fileManager'),
                
    $this->getDateTime(),
                
    $this->getInjection('number'),
                
    $this->getLanguage(),
                
    $templateFileManager
            
    );
        }

        public function 
    sendInvitations(Entity $entity$useUserSmtp true)
        {
            
    $invitationManager $this->getInvitationManager($useUserSmtp);

            
    $emailHash = array();

            
    $sentCount 0;

            
    $users $entity->get('users');
            foreach (
    $users as $user) {
                if (
    $user->id === $this->getUser()->id) {
                    if (
    $entity->getLinkMultipleColumn('users''status'$user->id) === 'Accepted') {
                        continue;
                    }
                }
                if (
    $user->get('emailAddress') && !array_key_exists($user->get('emailAddress'), $emailHash)) {
                    
    $invitationManager->sendInvitation($entity$user'users');
                    
    $emailHash[$user->get('emailAddress')] = true;
                    
    $sentCount ++;
                }
            }

            
    $contacts $entity->get('contacts');
            foreach (
    $contacts as $contact) {
                if (
    $contact->get('emailAddress') && !array_key_exists($contact->get('emailAddress'), $emailHash)) {
                    
    $invitationManager->sendInvitation($entity$contact'contacts');
                    
    $emailHash[$user->get('emailAddress')] = true;
                    
    $sentCount ++;
                }
            }

            
    $leads $entity->get('leads');
            foreach (
    $leads as $lead) {
                if (
    $lead->get('emailAddress') && !array_key_exists($lead->get('emailAddress'), $emailHash)) {
                    
    $invitationManager->sendInvitation($entity$lead'leads');
                    
    $emailHash[$user->get('emailAddress')] = true;
                    
    $sentCount ++;
                }
            }

            if (!
    $sentCount) return false;

            return 
    true;
        }

        public function 
    setAcceptanceStatus(string $idstring $status, ?string $userId null)
        {
            
    $userId $userId ?? $this->getUser()->id;

            
    $statusList $this->getMetadata()->get(['entityDefs'$this->entityType'fields''acceptanceStatus''options'], []);
            if (!
    in_array($status$statusList)) throw new BadRequest();

            
    $entity $this->getEntityManager()->getEntity($this->entityType$id);
            if (!
    $entity) throw new NotFound();
            if (!
    $entity->hasLinkMultipleId('users'$userId));


            
    $this->getEntityManager()->getRepository($this->entityType)->updateRelation(
                
    $entity'users'$userId, (object) ['status' => $status]
            );

            
    $actionData = [
                
    'eventName' => $entity->get('name'),
                
    'eventType' => $entity->getEntityType(),
                
    'eventId' => $entity->id,
                
    'dateStart' => $entity->get('dateStart'),
                
    'status' => $status,
                
    'link' => 'users',
                
    'inviteeType' => 'User',
                
    'inviteeId' => $userId,
            ];

            
    $this->getEntityManager()->getHookManager()->process($this->entityType'afterConfirmation'$entity, [], $actionData);

            return 
    true;
        }
    }
    8. Add the next code into the file /custom/Espo/Custom/Repositories/Event.php:

    PHP Code:
    <?php

    namespace Espo\Custom\Repositories;

    use 
    Espo\ORM\Entity;
    use 
    Espo\Core\Utils\Util;

    class 
    Event extends \Espo\Core\Templates\Repositories\Event
    {
         protected function 
    beforeSave(Entity $entity, array $options = [])
        {

            if (!
    $this->getConfig()->get('eventAssignedUserIsAttendeeDisabled')) {
                if (
    $entity->hasLinkMultipleField('assignedUsers')) {
                    
    $assignedUserIdList $entity->getLinkMultipleIdList('assignedUsers');
                    foreach (
    $assignedUserIdList as $assignedUserId) {
                        
    $entity->addLinkMultipleId('users'$assignedUserId);
                        
    $entity->setLinkMultipleName('users'$assignedUserId$entity->getLinkMultipleName('assignedUsers'$assignedUserId));
                    }
                } else {
                    
    $assignedUserId $entity->get('assignedUserId');
                    if (
    $assignedUserId) {
                        
    $entity->addLinkMultipleId('users'$assignedUserId);
                        
    $entity->setLinkMultipleName('users'$assignedUserId$entity->get('assignedUserName'));
                    }
                }
            }

            if (
    $entity->isNew()) {
                
    $currentUserId $this->getEntityManager()->getUser()->id;
                if (
                    
    $entity->hasLinkMultipleId('users'$currentUserId)
                    &&
                    (
                        !
    $entity->getLinkMultipleColumn('users''status'$currentUserId)
                        ||
                        
    $entity->getLinkMultipleColumn('users''status'$currentUserId) === 'None'
                    
    )
                ) {
                    
    $entity->setLinkMultipleColumn('users''status'$currentUserId'Accepted');
                }
            }
        }

    }
    9. Administration -> Rebuild.

    10. F5 to refresh a web page.

    Leave a comment:


  • Maximus
    replied
    Hi everyone. Here is an example of how you can build attendees field and Send Invitation logic for the custom entity. In my case, I used the custom Event type entity called "Event".

    Note: Due to max post size (10000 characters) on forum my solution is divided into 2 part.

    Part 1

    1. Add into the file /custom/Espo/Custom/Resources/metadata/entityDefs/Event.json:
    Code:
    }
        "fields": {
            "acceptanceStatus": {
                "type": "enum",
                "notStorable": true,
                "options": ["None", "Accepted", "Tentative", "Declined"],
                "style": {
                    "Accepted": "success",
                    "Declined": "danger",
                    "Tentative": "warning"
                },
                "layoutDetailDisabled": true,
                "layoutMassUpdateDisabled": true,
                "importDisabled": true,
                "exportDisabled": true,
                "where": {
                    "=": {
                        "leftJoins": ["users", "contacts", "leads"],
                        "sql": "contactsMiddle.status = {value} OR leadsMiddle.status = {value} OR usersMiddle.status = {value}",
                        "distinct": true
                    },
                    "<>": "[COLOR=#FF0000]event.id[/COLOR] NOT IN (SELECT [COLOR=#FF0000]event_id[/COLOR] FROM [COLOR=#FF0000]contact_event[/COLOR] WHERE deleted = 0 AND status = {value}) AND [COLOR=#FF0000]event.id[/COLOR] NOT IN (SELECT [COLOR=#FF0000]event_id[/COLOR] FROM [COLOR=#FF0000]event_user[/COLOR] WHERE deleted = 0 AND status = {value}) AND [COLOR=#FF0000]event.id[/COLOR] NOT IN (SELECT [COLOR=#FF0000]event_id[/COLOR] FROM [COLOR=#FF0000]event_lead[/COLOR] WHERE deleted = 0 AND status = {value})",
                    "IN": {
                        "leftJoins": ["users", "leads", "contacts"],
                        "sql": "contactsMiddle.status IN {value} OR leadsMiddle.status IN {value} OR usersMiddle.status IN {value}",
                        "distinct": true
                    },
                    "NOT IN": "[COLOR=#FF0000]event.id[/COLOR] NOT IN (SELECT [COLOR=#FF0000]event_id[/COLOR] FROM [COLOR=#FF0000]contact_event[/COLOR] WHERE deleted = 0 AND status IN {value}) AND [COLOR=#FF0000]event.id[/COLOR] NOT IN (SELECT [COLOR=#FF0000]event_id[/COLOR] FROM [COLOR=#FF0000]event_user[/COLOR] WHERE deleted = 0 AND status IN {value}) AND [COLOR=#FF0000]event.id[/COLOR] NOT IN (SELECT [COLOR=#FF0000]event_id[/COLOR] FROM [COLOR=#FF0000]event_lead[/COLOR] WHERE deleted = 0 AND status IN {value})",
                    "IS NULL": {
                        "leftJoins": ["users", "contacts", "leads"],
                        "sql": "contactsMiddle.status IS NULL AND leadsMiddle.status IS NULL AND usersMiddle.status IS NULL",
                        "distinct": true
                    },
                    "IS NOT NULL": "[COLOR=#FF0000]event.id[/COLOR] NOT IN (SELECT [COLOR=#FF0000]event_id[/COLOR] FROM [COLOR=#FF0000]contact_event[/COLOR] WHERE deleted = 0 AND status IS NULL) OR [COLOR=#FF0000]event.id[/COLOR] NOT IN (SELECT [COLOR=#FF0000]event_id[/COLOR] FROM [COLOR=#FF0000]event_user [/COLOR]WHERE deleted = 0 AND status IS NULL) OR [COLOR=#FF0000]event.id[/COLOR] NOT IN (SELECT [COLOR=#FF0000]event_id[/COLOR] FROM [COLOR=#FF0000]event_lead[/COLOR] WHERE deleted = 0 AND status IS NULL)"
                },
                "view": "crm:views/meeting/fields/acceptance-status"
            },
            "users": {
                "type": "linkMultiple",
                "view": "crm:views/meeting/fields/users",
                "layoutDetailDisabled": true,
                "layoutListDisabled": true,
                "columns": {
                    "status": "acceptanceStatus"
                },
                "additionalAttributeList": ["columns"],
                "orderBy": "name"
            },
            "contacts": {
                "type": "linkMultiple",
                "layoutDetailDisabled": true,
                "layoutListDisabled": true,
                "view": "crm:views/meeting/fields/contacts",
                "columns": {
                    "status": "acceptanceStatus"
                },
                "additionalAttributeList": ["columns"],
                "orderBy": "name"
            },
            "leads": {
                "type": "linkMultiple",
                "view": "crm:views/meeting/fields/attendees",
                "layoutDetailDisabled": true,
                "layoutListDisabled": true,
                "columns": {
                    "status": "acceptanceStatus"
                },
                "additionalAttributeList": ["columns"],
                "orderBy": "name"
            },
            .... other fields .....
        "links": {
            "users": {
                "type": "hasMany",
                "entity": "User",
                "foreign": "events",
                "additionalColumns": {
                    "status": {
                        "type": "varchar",
                        "len": "36",
                        "default": "None"
                    }
                }
            },
            "contacts": {
                "type": "hasMany",
                "entity": "Contact",
                "foreign": "events",
                "additionalColumns": {
                    "status": {
                        "type": "varchar",
                        "len": "36",
                        "default": "None"
                    }
                }
            },
            "leads": {
                "type": "hasMany",
                "entity": "Lead",
                "foreign": "events",
                "additionalColumns": {
                    "status": {
                        "type": "varchar",
                        "len": "36",
                        "default": "None"
                    }
                }
            },
            .... other links .....
    }
    Note: be sure that your tables names and fields are corresponded to DB.

    2. In the file /custom/Espo/Custom/Resources/metadata/clientDefs/Event.json you need to have this links to view:

    Code:
    {
        "controller": "controllers/record",
        "acl": "crm:acl/meeting",
        "views":{
            "detail":"crm:views/meeting/detail"
        },
        "recordViews": {
            "list":"crm:views/meeting/record/list",
            "detail":"crm:views/meeting/record/detail"
        },
        "modalViews": {
            "detail":"crm:views/meeting/modals/detail"
        },
        "activityDefs": {
            "activitiesCreate": true,
            "historyCreate": true
        },
        "sidePanels":{
            "detail":[
                {
                    "name":"attendees",
                    "label":"Attendees",
                    "view":"crm:views/meeting/record/panels/attendees",
                    "options":{
                        "fieldList":[
                            "users",
                            "contacts",
                            "leads"
                        ]
                    },
                    "sticked": true,
                    "isForm": true,
                    "notRefreshable": true
                }
            ],
            "detailSmall":[
                {
                    "name":"attendees",
                    "label":"Attendees",
                    "view":"crm:views/meeting/record/panels/attendees",
                    "sticked": true,
                    "isForm": true,
                    "notRefreshable": true
                }
            ],
            "edit":[
                {
                    "name":"attendees",
                    "label":"Attendees",
                    "view":"crm:views/meeting/record/panels/attendees",
                    "sticked": true,
                    "isForm": true,
                    "notRefreshable": true
                }
            ],
            "editSmall":[
                {
                    "name":"attendees",
                    "label":"Attendees",
                    "view":"crm:views/meeting/record/panels/attendees",
                    "sticked": true,
                    "isForm": true,
                    "notRefreshable": true
                }
            ]
        },
        ..... other your parameters .....
    }
    3. Add to the file /custom/Espo/Custom/Resources/metadata/entityDefs/Lead.json these fields and link:

    Code:
    {
        "fields": {
            "acceptanceStatus": {
                "type": "varchar",
                "notStorable": true,
                "exportDisabled": true,
                "disabled": true
            },
            "acceptanceStatusEvents": {
                "type": "enum",
                "notStorable": true,
                "directUpdateDisabled": true,
                "layoutAvailabilityList": ["filters"],
                "importDisabled": true,
                "exportDisabled": true,
                "view": "crm:views/lead/fields/acceptance-status",
                "link": "events",
                "column": "status"
            }
        },
        "links": {
            "events": {
                "type": "hasMany",
                "entity": "Event",
                "foreign": "leads",
                "layoutRelationshipsDisabled": true,
                "audited": true
            }
        }
    }
    4. Add to the file /custom/Espo/Custom/Resources/metadata/entityDefs/Contact.json these fields and link:

    Code:
    {
        "fields": {
            "acceptanceStatus": {
                "type": "varchar",
                "notStorable": true,
                "exportDisabled": true,
                "disabled": true
            },
            "acceptanceStatusEvents": {
                "type": "enum",
                "notStorable": true,
                "directUpdateDisabled": true,
                "layoutAvailabilityList": ["filters"],
                "importDisabled": true,
                "exportDisabled": true,
                "view": "crm:views/lead/fields/acceptance-status",
                "link": "events",
                "column": "status"
            }
        },
        "links": {
            "events": {
                "type": "hasMany",
                "entity": "Event",
                "foreign": "contacts",
                "layoutRelationshipsDisabled": true,
                "audited": true
            }
        }
    }

    Leave a comment:


  • fcm.moura
    replied
    Okay. I'll try the suggestions mentioned. Then I post the results here on the forum.
    Thanks

    Leave a comment:


  • espcrm
    replied
    Not sure if this is what you want but you can do these via GUI rather than coding. Currently this is good enough for me, in future I want to separate the History side panel into another area.

    There was one more screenshot I wanted create but I can't find where it is.

    I think this post might answer your question: https://www.eblasoft.com.tr/post/clo...ity-in-espocrm
    Last edited by espcrm; 03-11-2020, 08:03 AM.

    Leave a comment:


  • telecastg
    replied
    I'm sorry, I am not familiar with the internal mechanics of "attendees" which is translated as "Participant" in the Espo language files but I believe that a good source to learn about it would be to study the "Meeting" entity and see how "attendees" are handled there.

    In a "Meeting" entity, "attendees" are actually "Leads"

    "Meeting" and "Lead" are linked as a One-to-Many relationship

    The relevant scripts that you might want to check are:

    application\Espo\Modules\Crm\Resources\metadata\cl ientDefs\Meeting.json

    application\Espo\Modules\Crm\Resources\metadata\en tityDefs\Meeting.json

    client\modules\crm\views\meeting\fields\attendees. js

    client\modules\crm\views\meeting\record\panels\att endees.js

    Once you are able to implement your customization, it would be greatly appreciated if you can post the solution here.

    Best wishes
    Last edited by telecastg; 03-11-2020, 05:59 AM.

    Leave a comment:

Working...
X