Magento 2: A Helpdesk Module - Help Desk Tickets System (Part - 2)

A full Magento 2 module for helpdesk tickets system with frontend and backend interface. This tutorial is the backend

Posted on September 5, 2017 in Magento2

Outline of the backend interface

  • ACL resource used to allow or disallow access to the ticket listing
  • Menu item linking to tickets listing the constroller action
  • Route that maps to our admin controller
  • layout XMLs that map to the ticket listing the controller action
  • Controller action for listing tickets
  • Full XML layout grid definition within layout XMLs defining grid, custom column renders, and custom dropdown filter values
  • Controller action for closing ticket and send e-mails to customers

Creating Access Control Lists (acl.xml)

The app/code/Jeff/Helpdesk/etc/acl.xml file is where we define our module access control list resources.


#app/code/Jeff/Helpdesk/etc/acl.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
    <acl>
        <resources>
            <resource id="Magento_Backend::admin">
                <resource id="Magento_Customer::customer">
                    <resource id="Jeff_Helpdesk::ticket_manager" title="Manage Helpdesk Tickets"/>
                </resource>
                <resource id="Magento_Backend::stores">
                    <resource id="Magento_Backend::stores_settings">
                        <resource id="Magento_Config::config">
                            <resource id="Jeff_Helpdesk::helpdesk" title="Helpdeskt Section" />
                        </resource>
                    </resource>
                </resource>
            </resource>
        </resources>
    </acl>
</config>

Access control list resources are visible under the Magento admin System | Permission | User Role, as shown in the following screenshot:

access control list

Creating menu item linking to the tickets listing the controller acction.


#app/code/Jeff/Helpdesk/etc/adminhtml/menu.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd">
    <menu>
        <add id="Jeff_Helpdesk::ticket_manage" title="Helpdesk Tickets" module="Jeff_Helpdesk" parent="Magento_Customer::customer" action="jeff_helpdesk/ticket/index" resource="Jeff_Helpdesk::ticket_manage" />
    </menu>
</config>

The resource attribute references the ACL resource defined in the app/code/Jeff/Helpdesk/etc/acl.xml file.

Creating routes, controllers and layout hanldes


#app/code/Jeff/Helpdesk/etc/adminhtml/routes.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="admin">
        <route id="jeff_helpdesk" frontName="jeff_helpdesk">
            <module name="Jeff_Helpdesk"/>
        </route>
    </router>
</config>

The admin route definition is almost identical to the frontend router defintion, where the differnce is for the router ID value.

With the router definition in place, we can now define our three layout XMLs, which map to the ticket listing the controller action:

  • jeff_helpdesk_ticket_index.xml
  • jeff_helpdesk_ticket_grid.xml
  • jeff_helpdesk_ticket_grid_block.xml

jeff_helpdesk_ticket_index.xml


#app/code/Jeff/Helpdesk/view/adminhtml/layout/jeff_helpdesk_ticket_index.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <update handle="formkey" />
    <update handle="jeff_helpdesk_ticket_grid_block" />
    <body>
        <referenceContainer name="content">
            <block class="Jeff\Helpdesk\Block\Adminhtml\Ticket" name="admin.block.helpdesk.ticket.grid.container">
            </block>
        </referenceContainer>
    </body>
</page>

Two update handles are specified. One pulls in formkey and the other pulls in jeff_helpdesk_ticket_grid_block.

jeff_helpdesk_ticket_grid.xml


#app/code/Jeff/Helpdesk/view/adminhtml/layout/jeff_helpdesk_ticket_grid.xml

<?xml version="1.0"?>
<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd">
    <update handle="formkey" />
    <update handle="jeff_helpdesk_ticket_grid_block" />
    <container name="root">
        <block class="Magento\Backend\Block\Widget\Grid\Container" name="admin.block.helpdesk.ticket.grid.container" template="Magento_Backend::widget/grid/container/empty.phtml" />
    </container>
</layout>

jeff_helpdesk_ticket_grid_block.xml


#app/code/Jeff/Helpdesk/view/adminhtml/layout/jeff_helpdesk_ticket_grid_block.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="admin.block.helpdesk.ticket.grid.container">
            <block class="Magento\Backend\Block\Widget\Grid" name="admin.block.helpdesk.ticket.grid" as="grid">
                <arguments>
                    <argument name="id" xsi:type="string">ticketGrid</argument>
                    <argument name="dataSource" xsi:type="object">Jeff\Helpdesk\Model\ResourceModel\Ticket\Collection</argument>
                    <argument name="default_sort" xsi:type="string">ticket_id</argument>
                    <argument name="default_dir" xsi:type="string">desc</argument>
                    <argument name="save_parameters_in_session" xsi:type="boolean">true</argument>
                    <argument name="use_ajax" xsi:type="boolean">true</argument>
                </arguments>
                <block class="Magento\Backend\Block\Widget\Grid\ColumnSet" name="admin.block.helpdesk.ticket.grid.columnSet" as="grid.columnSet">
                    <block class="Magento\Backend\Block\Widget\Grid\Column" as="ticket_id">
                        <arguments>
                            <argument name="header" xsi:type="string" translate="true">ID</argument>
                            <argument name="type" xsi:type="string">number</argument>
                            <argument name="id" xsi:type="string">ticket_id</argument>
                            <argument name="index" xsi:type="string">ticket_id</argument>
                        </arguments>
                    </block>

                    <block class="Magento\Backend\Block\Widget\Grid\Column" as="title">
                        <arguments>
                            <argument name="header" xsi:type="string" translate="true">Title</argument>
                            <argument name="type" xsi:type="string">string</argument>
                            <argument name="id" xsi:type="string">title</argument>
                            <argument name="index" xsi:type="string">title</argument>
                        </arguments>
                    </block>

                    <block class="Magento\Backend\Block\Widget\Grid\Column" as="severity">
                        <arguments>
                            <argument name="header" xsi:type="string" translate="true">Severity</argument>
                            <argument name="index" xsi:type="string">severity</argument>
                            <argument name="type" xsi:type="string">options</argument>
                            <argument name="options" xsi:type="options" model="Jeff\Helpdesk\Model\Ticket\Grid\Severity"/>
                            <argument name="renderer" xsi:type="string">Jeff\Helpdesk\Block\Adminhtml\Ticket\Grid\Renderer\Severity</argument>
                            <!-- <argument name="id" xsi:type="string">severity</argument> -->
                            <argument name="header_css_class" xsi:type="string">col-form_id</argument>
                            <argument name="column_css_class" xsi:type="string">col-form_id</argument>
                        </arguments>
                    </block>

                    <block class="Magento\Backend\Block\Widget\Grid\Column" as="status">
                        <arguments>
                            <argument name="header" xsi:type="string" translate="true">Status</argument>
                            <argument name="index" xsi:type="string">Status</argument>
                            <argument name="type" xsi:type="string">options</argument>
                            <argument name="options" xsi:type="options" model="Jeff\Helpdesk\Model\Ticket\Grid\Status"/>
                            <argument name="renderer" xsi:type="string">Jeff\Helpdesk\Block\Adminhtml\Ticket\Grid\Renderer\Status</argument>
                            <!-- <argument name="id" xsi:type="string">severity</argument> -->
                            <argument name="header_css_class" xsi:type="string">col-form_id</argument>
                            <argument name="column_css_class" xsi:type="string">col-form_id</argument>
                        </arguments>
                    </block>

                    <block class="Magento\Backend\Block\Widget\Grid\Column" as="action">
                        <arguments>
                            <argument name="id" xsi:type="string">action</argument>
                            <argument name="header" xsi:type="string" translate="true">Action</argument>
                            <argument name="type" xsi:type="string">action</argument>
                            <argument name="getter" xsi:type="string">getId</argument>
                            <argument name="filter" xsi:type="boolean">false</argument>
                            <argument name="sortable" xsi:type="boolean">false</argument>
                            <argument name="actions" xsi:type="array">
                                <item name="view_action" xsi:type="array">
                                    <item name="caption" xsi:type="string" translate="true">Close</item>
                                    <item name="url" xsi:type="array">
                                        <item name="base" xsi:type="string">*/*/close</item>
                                    </item>
                                    <item name="field" xsi:type="string">id</item>
                                </item>
                            </argument>
                            <argument name="header_css_class" xsi:type="string">col-form_id</argument>
                            <argument name="column_css_class" xsi:type="string">col-form_id</argument>
                        </arguments>
                    </block>
                </block>
            </block>
        </referenceBlock>
    </body>
</page>
<!--
required field are : header, type, id, index
optional field are : options, renderer, header_css_class, column_css_class 
-->

Utilizing the grid widget: Ticket.php


#app/code/Jeff/Helpdesk/Block/Adminhtml/Ticket.php

<?php
namespace Jeff\Helpdesk\Block\Adminhtml;

use Magento\Backend\Block\Widget\Grid\Container;

class Ticket extends Container {
    protected function _construct() {
        $this->_controller = 'adminhtml';
        $this->_blockGroup = 'Jeff_Helpdesk';
        $this->_headerText = __('Tickets');

        parent::_construct();
        $this->removeButton('add');
    }
}
/*
Not much is happening in the Ticket Block class here.
_controller and _blockGroup, as these serve as a sort of glue for telling our grid where to find
other possible block classes. 
*/

Two grid column renderers

Severity.php File:


#app/code/Jeff/Helpdesk/Block/Adminhtml/Ticket/Grid/Renderer/Severity.php

<?php
namespace Jeff\Helpdesk\Block\Adminhtml\Ticket\Grid\Renderer;

use Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer;
use Magento\Backend\Block\Context;
use Jeff\Helpdesk\Model\TicketFactory;

class Severity extends AbstractRenderer {
    protected $ticketFactory;

    public function __construct(Context $context, TicketFactory $ticketFactory, array $data = []) {
        parent::__construct($context, $data);

        $this->ticketFactory = $ticketFactory;
    }

    public function render(\Magento\Framework\DataObject $row) {
        $ticket = $this->ticketFactory->create()->load($row->getId());

        if($ticket && $ticket->getId()) {
            return $ticket->getSeverityAsLabel();
        }

        return '';
    }
}

Status.php file:


#app/code/Jeff/Helpdesk/Block/Adminhtml/Ticket/Grid/Renderer/Status.php

<?php
namespace Jeff\Helpdesk\Block\Adminhtml\Ticket\Grid\Renderer;

use Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer;
use Magento\Backend\Block\Context;
use Jeff\Helpdesk\Model\TicketFactory;

class Status extends AbstractRenderer {
    protected $ticketFactory;

    public function __construct(Context $context, TicketFactory $ticketFactory, array $data = []) {
        parent::__construct($context, $data);

        $this->ticketFactory = $ticketFactory;
    }

    public function render(\Magento\Framework\DataObject $row) {
        $ticket = $this->ticketFactory->create()->load($row->getId());

        if($ticket && $ticket->getId()) {
            return $ticket->getStatusAsLabel();
        }

        return '';
    }
}

Creating controller actions

abstract class Ticket.php for all the adminhtml controllers to extend.


# app/code/Jeff/Helpdesk/Controller/Adminhtml/Ticket.php

<?php
namespace Jeff\Helpdesk\Controller\Adminhtml;

use Magento\Backend\App\Action\Context;
use Magento\Backend\App\Action as BackendAction;
use Magento\Framework\View\Result\PageFactory;
use Magento\Backend\Model\View\Result\ForwardFactory;

abstract class Ticket extends BackendAction {
    protected $resultPageFactory;
    protected $resultForwardFactory;
    protected $resultRedirectFactory;

    public function __construct(Context $context, PageFactory $resultPageFactory, ForwardFactory $resultForwardFactory) {
        $this->resultPageFactory = $resultPageFactory;
        $this->resultForwardFactory = $resultForwardFactory;
        $this->resultRedirectFactory = $context->getResultRedirectFactory();
        parent::__construct($context);
    }

    protected function _isAllowed() {
        return $this->_authorization->isAllowed('Jeff_Helpdesk::ticket_manage');
    }

    protected function _initAction() {
        $this->_view->loadLayout();
        $this->_setActiveMenu('Jeff_Helpdesk::ticket_manage')->_addBreadcrumb(__('Helpdesk'), __('Tickets'));

        return $this;
    }
}
/*
$resultPageFactory, $resultForwardFactory, $resultRedirectFactory are object to be used on the child.
*/

Index action:


#app/code/Jeff/Helpdesk/Controller/Adminhtml/Ticket/Index.php

<?php
namespace Jeff\Helpdesk\Controller\Adminhtml\Ticket;
class Index extends \Jeff\Helpdesk\Controller\Adminhtml\Ticket
{
    public function execute()
    {
        //return $this->resultPageFactory->create();  
        if($this->getRequest()->getQuery('ajax')) {
            $resultForward = $this->resultForwardFactory->create();
            $resultForward->forward('grid');
            return $resultForward;
        }
        $resultPage = $this->resultPageFactory->create();

        $resultPage->setActiveMenu('Jeff_Helpdesk::ticket_manage');
        $resultPage->getConfig()->getTitle()->prepend(__('Tickets'));

        $resultPage->addBreadcrumb(__('Tickets'), __('Tickets'));
        $resultPage->addBreadcrumb(__('Manage Tickets'), __('Manage Tickets'));


        return $resultPage;
    }
}

Grid action:


#app/code/Jeff/Helpdesk/Controller/Adminhtml/Ticket/Grid.php

<?php
namespace Jeff\Helpdesk\Controller\Adminhtml\Ticket;

class Grid extends \Jeff\Helpdesk\Controller\Adminhtml\Ticket {
    public function execute() {
        $this->_view->loadLayout(false);
        $this->_view->renderLayout();
    }
}
/*
However, if we now try to use sorting or filtering, we would get a broken layout.
This is because based on arguments defined under the jeff_helpdesk_ticket_grid_block.xml file,
we are missing the controller Grid action. 
When we use sorting or filtering, the AJAX request hits the Index controller Grid Action.
The code within thex execute method simplye calls the loadLayout(false) method to prevent
the entire layout loading, making it load only the bits defined under the jeff_helpdesk_ticket_grid.xml fiel.
*/

Close action:


#app/code/Jeff/Helpdesk/Controller/Adminhtml/Ticket/Close.php

<?php
namespace Jeff\Helpdesk\Controller\Adminhtml\Ticket;

use Magento\Backend\App\Action\Context;
use Magento\Framework\View\Result\PageFactory;
use Magento\Backend\Model\View\Result\ForwardFactory;
use Jeff\Helpdesk\Model\TicketFactory;
use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Framework\Mail\Template\TransportBuilder;
use Magento\Framework\Translate\Inline\StateInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\StoreManagerInterface;

class Close extends \Jeff\Helpdesk\Controller\Adminhtml\Ticket {
    
    protected $ticketFactory;
    protected $customerRepository;
    protected $transportBuilder;
    protected $inlineTranslation;
    protected $scopeConfig;
    protected $storeManager;

    public function __construct(
        Context $context, 
        PageFactory $resultPageFactory, 
        ForwardFactory $resultForwardFactory, 
        TicketFactory $ticketFactory,
        CustomerRepositoryInterface $customerRepository,
        TransportBuilder $transportBuilder,
        StateInterface $inlineTranslation,
        ScopeConfigInterface $scopeConfig,
        StoreManagerInterface $storeManager
    ) {
        $this->ticketFactory = $ticketFactory;
        $this->customerRepository = $customerRepository;
        $this->transportBuilder = $transportBuilder;
        $this->inlineTranslation = $inlineTranslation;
        $this->scopeConfig = $scopeConfig;
        $this->storeManager = $storeManager;
        parent::__construct($context, $resultPageFactory, $resultForwardFactory);
    }

    public function execute() {
        $ticketId = $this->getRequest()->getParam('id');
        $ticket = $this->ticketFactory->create()->load($ticketId);

        if($ticket && $ticket->getId()) {
            try{
                $ticket->setStatus(\Jeff\Helpdesk\Model\Ticket::STATUS_CLOSED);
                $ticket->save();
                $this->messageManager->addSuccess(__('Ticket successfully closed.'));

                // Send email to customer

                $this->messageManager->addSuccess(__('Customer notified via email'));
            }
            catch(Exception $e) {
                $this->messageManager->addError(__('Error with closing ticket action'));
            }
        }

        $resultRedirect = $this->resultRedirectFactory->create();
        $resultRedirect->setPath('*/*/index');

        return $resultRedirect;
    }
}

The Close action has two separate roles to fulfill. One is to change the ticket status; the other is to send an e-mail to the customer using the proper e-mail template

At this point, we should be able to see the screen, as shown in the following screenshot:

backend grid for helpdesk ticket system

Summary

With this tutorial, “Helpdesk Tickets System”, we covered quite a lot of various Magento 2 platform parts, from routes, ACLs, controllers, blocks, XML layouts, grids, controller actions, models, resources, collections, install scripts, email templates, email transport, and layout.
You could download the code from Github
Enjoy!!!!


comments powered by Disqus