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

A full Magento 2 module for helpdesk tickets system with frontend and backend interface

Posted on September 1, 2017 in Magento2

A New Magento 2 module for Helpdesk Ticket System

The ticket system requirement:

  • Name used, Jeff/Helpdesk
  • Data to be stored in table is called jeff_helpdesk_ticket
  • Tickets entity will contain ticket_id, customer_id, title, severity, created_at and status attributes
  • Two email templates: store_owner_to_customer_email_template and customer_to_store_owner_email_template are to be defined for pushing email updates ticket creation and status change
  • Customers will be able to submit a ticket through their “MY ACCOUNT” section.
  • Customers will be able to see all of their previously submitted tickets under their “My Account” section
  • Customer will not be able to edit any existing tickets
  • Once a customer submits a new ticket, transactional email will be sent to the store owner.
  • Admin users will be able to access a list of all tickets under “Customers | Helpdesk Tickets”.
  • Once Admin users change the ticket status, an email will be sent to the customer.

Create a new module by creating registration.php and module.xml

registration.php file:


<?php
#app/code/Jeff/Helpdesk/registration.php

    \Magento\Framework\Component\ComponentRegistrar::register(
        \Magento\Framework\Component\ComponentRegistrar::MODULE,
        'Jeff_Helpdesk',
        __DIR__
    );

module.xml file:

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

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Jeff_Helpdesk" setup_version="0.0.1"/>
</config>

Create Database setup script

InstallSchema.php file:


#app/code/Jeff/Helpdesk/Setup/InstallSchema.php

<?php

namespace Jeff\Helpdesk\Setup;

use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;

/**
 * @codeCoverageIgnore
 */
class InstallSchema implements InstallSchemaInterface {
    //InstallSchemaInterface, ModuleContextInterface, SchemaSetupInterface
    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) {
        $installer = $setup;

        $installer->startSetup();

        $table = $installer->getConnection()
            ->newTable($installer->getTable('jeff_helpdesk_ticket'))
            ->addColumn(
                'ticket_id',
                \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
                null,
                ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true],
                'Ticket Id'
            )
            ->addColumn(
                'customer_id',
                \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
                null,
                ['unsigned' => true],
                'Customer Id'
            )
            ->addColumn(
                'title',
                \Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
                null,
                ['nullable' => false],
                'Title'
            )
            ->addColumn(
                'severity',
                \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
                null,
                ['nullable' => false],
                'Severity'
            )
            ->addColumn(
                'created_at',
                \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP,
                null,
                ['nullable' => false],
                'Created At'
            )
            ->addColumn(
                'status',
                \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
                null,
                ['nullable' => false],
                'Severity'
            )
            ->addIndex(
                $installer->getIdxName('jeff_helpdesk_ticket', ['customer_id']),
                ['customer_id']
            )
            ->addForeignKey(
                $installer->getFkName('jeff_helpdesk_ticket', 'customer_id', 'customer_entity', 'entity_id'),
                'customer_id',
                $installer->getTable('customer_entity'),
                'entity_id',
                \Magento\Framework\DB\Ddl\Table::ACTION_SET_NULL
            );

        $installer->getConnection()->createTable($table);

        $installer->endSetup();
    }
}

Create Model and Resource Model file for Database CRUD

Ticket.php file:

#app/code/Jeff/Helpdesk/Model/Ticket.php

<?php
namespace Jeff\Helpdesk\Model;

class Ticket extends \Magento\Framework\Model\AbstractModel {
    const STATUS_OPENED = 1;
    const STATUS_CLOSED = 2;

    const SEVERITY_LOW = 1;
    const SEVERITY_MEDIUM = 2;
    const SEVERITY_HIGH = 3;


    protected static $statusesOptions = [self::STATUS_OPENED=>'Opened', self::STATUS_CLOSED=>'Closed'];

    protected static $severitiesOptions = [self::SEVERITY_LOW=>'Low', self::SEVERITY_MEDIUM=>'Medium', self::SEVERITY_HIGH=>'High'];

    /**
     * Initialize resource model
     * @return void
     */
    protected function _construct()
    {
        $this->_init('Jeff\Helpdesk\Model\ResourceModel\Ticket');
    }

    public static function getSeveritiesOptionArray() {
        return self::$severitiesOptions;
    }

    public static function getStatusesOptionArray() {
        return self::$statusesOptions;
    }

    public function getStatusAsLabel() {
        return self::$statusesOptions[$this->getStatus()];
    }

    public  function getSeverityAsLabel() {
        return self::$severitiesOptions[$this->getSeverity()];
    }
}

Ticket.php resource model file:

#app/code/Jeff/Helpdesk/Model/ResourceModel/Ticket.php

<?php
namespace Jeff\Helpdesk\Model\ResourceModel;

use Magento\Framework\Model\ResourceModel\Db\AbstractDb;

class Ticket extends AbstractDb {
    protected function _construct() {
        $this->_init('jeff_helpdesk_ticket', 'ticket_id');
    }
}

model's collection file:

#app/code/Jeff/Helpdesk/Model/ResourceModel/Ticket/Collection.php

<?php
namespace Jeff\Helpdesk\Model\ResourceModel\Ticket;

use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;

class Collection extends AbstractCollection {
    protected function _construct() {
        $this->_init('Jeff\Helpdesk\Model\Ticket', 'Jeff\Helpdesk\Model\ResourceModel\Ticket');
    }
}

Module's backend config file to set up default email templates

config.xml file:

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

<?xm version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <jeff_helpdesk>
            <email_template>
                <customer>
                    jeff_helpdesk_email_template_customer
                </customer>
                <store_owner>
                    jeff_helpdesk_email_template_store_owner
                </store_owner>
            </email_template>
        </jeff_helpdesk>
    </default>
</config>

Register two email templates for our system

email_templates.xml file:


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

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Email:etc/email_template.xsd">
    <template id="jeff_helpdesk_email_template_customer" label="Jeff Helpdesk - Customer Email" file="store_owner_to_customer.html" type="html" module="Jeff_Helpdesk" area="frontend" />
    <template id="jeff_helpdesk_email_template_store_owner" label="Jeff Helpdesk - Store Owner Email" file="customer_to_store_owner.html" type="html" module="Jeff_Helpdesk" area="frontend" />
</config>
*Note:
The values of the "file" attribute point to the location of the following files:
1) app/code/Jeff/Helpdesk/view/frontend/email/store_owner_to_customer.html
2) app/code/Jeff/Helpdesk/view/frontend/email/customer_to_owner.html

Create two email templates

customer_to_store_owner.html file:


#app/code/Jeff/Helpdesk/view/frontend/email

<!-- @subject New Ticket Created @-->
<h1>Ticket #{{var ticket.ticket_id}} created</h1>
<ul>
    <li>Id: {{var ticket.ticket_id}}</li>
    <li>Title: {{var ticket.title}}</li>
    <li>Created_at: {{var ticket.created_at}}</li>
    <li>Severity: {{var ticket.serverity}}</li>
</ul>

store_owner_to_customer.html file:


#app/code/Jeff/Helpdesk/view/frontend/email/store_owner_to_customer.html

<!-- @subjt Ticket Updated @-->
<h1>Ticket #{{var ticket.ticket_id}} updated</h1>
<p>Hi {{var customer_name}}</p>
<p>Status of your ticket #{var ticket.ticket_id}} has been updated</p>

<ul>
    <li>Title: {{var ticket.title}}</li>
    <li>Created at: {{var ticket.created_at}}</li>
    <li>Severity: {{var ticket.severity}}</li>
</ul>

Set up the frontend route

routes.xml file:


#app/code/Jeff/Helpdesk/etc/frontend/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="standard">
        <route id="jeff_helpdesk" frontName="jeff_helpdesk">
            <module name="Jeff_Helpdesk"/>
        </route>
    </router>
</config>

Create the frontend Block

Index.php block file:


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

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

use Magento\Framework\View\Element\Template;
use Magento\Framework\View\Element\Template\Context;
use Magento\Framework\Stdlib\DateTime;
use Magento\Customer\Model\Session as CustomerSession;
use Jeff\Helpdesk\Model\TicketFactory;

class Index extends Template {
    /**
     * @var \Magento\Framework\Stdlib\DateTime
     */
    protected $dateTime;

    /**
     * @var \Magento\Customer\Model\Session
     */
    protected $customerSession;

    /**
     * @var \Jeff\Helpdesk\Model\TicketFactory
     */
    protected $ticketFactory;

    /**
     * @param \Magento\Framework\View\Element\Template\Context $context
     * @parma array $data
     */
    public function __construct(Context $context, DateTime $dateTime, CustomerSession $customerSession, TicketFactory $ticketFactory, array $data =[]) {
        $this->dateTime = $dateTime;
        $this->customerSession = $customerSession;
        $this->ticketFactory = $ticketFactory;
        parent::__construct($context, $data);
    }

    /**
     * @return \Jeff\Helpdesk\Model\ResourceModel\Ticket\Collection
     */
    public function getTickets() {
        return $this->ticketFactory->create()
            ->getCollection()
            ->addFieldToFilter('customer_id', $this->customerSession->getcustomerId())
            ->setOrder('ticket_id', 'DESC');
    }

    public function getSeverities() {
        return \Jeff\Helpdesk\Model\Ticket::getSeveritiesOptionArray();
    }
}

Create frontend controller files

Ticket.php --abstract controller file

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

<?php
namespace Jeff\Helpdesk\Controller;

use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\RequestInterface;
use Magento\Customer\Model\Session as CustomerSession;

abstract class Ticket extends Action {
    protected $customerSession;

    public function __construct(Context $context, CustomerSession $customerSession) {
        $this->customerSession = $customerSession;
        parent::__construct($context);
    }

    public function dispatch(RequestInterface $request) {
        if(!$this->customerSession->authenticate()) {
            $this->_actionFlag->set('', 'no-dispatch', true);

            if(!$this->customerSession->getBeforeUrl()) {
                $this->customerSession->setBeforeUrl($this->_redirect->getRefererUrl());
            }
        }

        return parent::dispatch($request);
    }
}
/*
our controller loads the customer session object through its constructor. The customer session object is then
used within the dispatch method to check if the customer is authenticated or not. 
*/

Index Action file Index.php:

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

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

class Index extends \Jeff\Helpdesk\Controller\Ticket
{

    public function execute()
    {
        $resultPage = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_PAGE);

        $resultPage->getConfig()->getTitle()->set(__('Help Tickets'));
        return $resultPage;
    }
}

Save action file: Save.php

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

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

use Magento\Framework\App\Action\Context;
use Magento\Customer\Model\Session as CustomerSession;
use Magento\Framework\Mail\Template\TransportBuilder;
use Magento\Framework\Translate\Inline\StateInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Framework\Data\Form\FormKey\Validator;
use Magento\Framework\Stdlib\DateTime;
use Jeff\Helpdesk\Model\TicketFactory;

class Save extends \Jeff\Helpdesk\Controller\Ticket {
    protected $transportBuilder;
    protected $inlineTranslation;
    protected $scopeConfig;
    protected $storeManager;
    protected $formKeyValidator;
    protected $dateTime;
    protected $ticketFactory;

    public function __construct(
        Context $context, 
        CustomerSession $customerSession, 
        TransportBuilder $transportBuilder, 
        StateInterface $inlineTranslation, 
        ScopeConfigInterface $scopeConfig, 
        StoreManagerInterface $storeManager, 
        Validator $formKeyValidator, 
        DateTime $dateTime, 
        TicketFactory $ticketFactory
    ) {
        
        $this->transportBuilder = $transportBuilder;
        $this->inlineTranslation = $inlineTranslation;
        $this->scopeConfig = $scopeConfig;
        $this->storeManager = $storeManager;
        $this->formKeyValidator = $formKeyValidator;
        $this->dateTime = $dateTime;
        $this->ticketFactory = $ticketFactory;
        $this->messageManager = $context->getMessageManager(); //get Message block
        parent::__construct($context, $customerSession);
    }

    public function execute() {
        $resultRedirect = $this->resultRedirectFactory->create();

        //var_dump($this->getRequest()->getParam('title'));
        //var_dump($this->getRequest()->getParam('severity'));

        if(!$this->formKeyValidator->validate($this->getRequest())) {
            return $resultRedirect->setRefererUrl();
        }
        $title = $this->getRequest()->getParam('title');
        $severity = $this->getRequest()->getParam('severity');

        try{
            $ticket = $this->ticketFactory->create();
            $ticket->setCustomerId($this->customerSession->getCustomerId());
            $ticket->setTitle($title);
            $ticket->setSeverity($severity);
            $ticket->setCreatedAt($this->dateTime->formatDate(true));
            $ticket->setStatus(\Jeff\Helpdesk\Model\Ticket::STATUS_OPENED);
            $ticket->save();

            $customer = $this->customerSession->getCustomerData();
            /*
            //Send email to store owner
            $storeScope = \Magento\Store\Model\ScopeInterface::SCOPE_STORE;
            $transport = $this->transportBuilder->setTemplateIdentifier($this->scopeConfig->getValue('jeff_helpdesk/email_template/store_owner', $storeScope))
                ->setTemplateOptions(
                    [
                        'area' => \Magento\Framework\App\Area::AREA_FRONTEND,
                        'store' => $this->storeManager->getStore()->getId(),
                    ]
                )
                ->setTemplateVars(['ticket' => $ticket])
                ->setFrom([
                    'name' => $customer->getFirstName. ' ' . $customer->getLastname(), 
                    'email' => $customer->getEmail()
                ])
                ->addTo($this->scopeConfig->getValue('trans_email/ident_general/email', $storeScope))
                ->getTransport();
            $transport->sendMessage();
            $this->inlineTranslation->resume();
            */

            $this->messageManager->addSuccess(__('Ticket successfully created.'));
        }
        catch(Exception $e) {
            $this->messageManager->addError(__('Error occurred during tickete creation.'));
        }

        return $resultRedirect->setRefererUrl();
    }
}

Two Layout files to add a link to the navigation of customer account panel

customer_account.xml file:

#app/code/Jeff/Helpdesk/view/frontend/layout/customer_account.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">
    <head>
        <title>Helpdesk Tickets</title>
    </head>
    <body>
        <referenceBlock name="customer_account_navigation">
            <block class="Magento\Framework\View\Element\Html\Link\Current" name="jeff-helpdesk-ticket">
                <arguments>
                    <argument name="path" xsi:type="string">jeff_helpdesk/ticket</argument>
                    <argument name="label" xsi:type="string">Helpdesk Tickets</argument>
                </arguments>
            </block>
            <move element="jeff-helpdesk-ticket" destination="customer_account_navigation" after="customer-account-navigation-orders-link" />
            <referenceBlock name="customer-account-navigation-billing-agreements-link" remove="true"/>
            <referenceBlock name="customer-account-navigation-downloadable-products-link" remove="true"/>
            <referenceBlock name="customer-account-navigation-wish-list-link" remove="true"/>
        </referenceBlock>
    </body>
</page>
<!--
Note:
Simply including the customer_account handle is not enough; we need something extra to define our link under the My Account section.
We define this extra something under the "app/code/Jeff/Helpdesk/view/frontend/layout/customer_account.xml"
What is happening here is that we are referencing an existing block called customer_account_navigation and defining a new block within it of class 
Magento\Framework\View\Element\Html\Link\Current. 
This block accepts two parameters: the path taht is set to our controller action and the label that is set to Helpdesk Tickets.
-->

jeff_helpdesk_ticket_index.xml file:


#app/code/Jeff/Helpdesk/view/frontend/layout/jeff_helpdesk_ticket_index.html

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="2columns-left" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <update handle="customer_account" />
<!--
    <head>
        <title>Help Deskt Tickets</title>
        <css src="Jeff_Helpdesk::css/help.css" />
    </head>
-->
    <body>
        <referenceBlock name="page.main.title" remove="true" />
        <referenceContainer name="content">
            <block template="Jeff_Helpdesk::ticket/index.phtml" class="Jeff\Helpdesk\Block\Ticket\Index" name="jeff.helpdesk.ticket.index" cacheable="false" />
        </referenceContainer>
    </body>

</page>
<!--
Notice how we immediately call the update directive, passing it the customer_account handle attribute value.
This is like saying, "Include everything from the customer_account handle into our handle here."
-->

Install and update our system


php bin/magento module:enable Jeff_Helpdesk
php bin/magento setup:upgrade
php bin/magento cache:clean
php bin/magento cache:flush

If you go to your account in the frontend, you will notice there is a new link “Helpdesk Tickets” at the left sidebar as following: template screenshot

Conclusion:

This “part 1” is the first part of Helpdesk Ticket system for Magento 2. In the tutorial, I presented how to create a new module for Magento 2 and add a link to the navigation of customer account panel. In the next part, I will present the steps to create backend for the Helpdesk Ticke system.
You could download the code from Github


comments powered by Disqus