From 523dc8d68f4e51ce3568052811230951a31ddf7d Mon Sep 17 00:00:00 2001
From: Peter Rotich <peter@osticket.com>
Date: Tue, 6 Jun 2017 06:03:05 +0000
Subject: [PATCH] Thread Referral

Add the initial concept of thread (tickets & tasks) referral.

* Agent & Team
Referring a ticket to an agent or team - is just like assignment without
actually assigning. If referred agent doesn't have access to the ticket's
department then they'll only have view access.

* Department
This is like assigning a ticket to the entire department. Meaning agents who
have access to the referred to department will now see the ticket - what
they can do with the ticket will depend on the assigned role in that
particular department.

It's important to note the ticket will technically be still the
responsibility of the primary department. Transferring the ticket is the
sure way to relinquishing the responsibility.

* Auto Department Referral via email
This will happen if an email is sent to multiple departments. For example an
email with TO: support &  CC: billing will result in the ticket getting
routed to "support" department and "billing" getting a referral of the same
ticket. This allows both departments to have visibility of the ticket -
which is not possible at the moment.
---
 bootstrap.php                          |   1 +
 include/ajax.tickets.php               |  85 +++++++++++++++
 include/class.forms.php                | 141 +++++++++++++++++++++++--
 include/class.model.php                |   6 ++
 include/class.staff.php                |  28 ++++-
 include/class.thread.php               | 133 +++++++++++++++++++++++
 include/class.ticket.php               | 103 +++++++++++++++---
 include/staff/templates/refer.tmpl.php | 129 ++++++++++++++++++++++
 include/staff/ticket-view.inc.php      |  27 +++++
 scp/ajax.php                           |   2 +
 10 files changed, 635 insertions(+), 20 deletions(-)
 create mode 100644 include/staff/templates/refer.tmpl.php

diff --git a/bootstrap.php b/bootstrap.php
index 8a68c1843..ff09b15b6 100644
--- a/bootstrap.php
+++ b/bootstrap.php
@@ -98,6 +98,7 @@ class Bootstrap {
         define('TICKET_TABLE',$prefix.'ticket');
         define('TICKET_CDATA_TABLE', $prefix.'ticket__cdata');
         define('THREAD_EVENT_TABLE',$prefix.'thread_event');
+        define('THREAD_REFERRAL_TABLE',$prefix.'thread_referral');
         define('THREAD_COLLABORATOR_TABLE', $prefix.'thread_collaborator');
         define('TICKET_STATUS_TABLE', $prefix.'ticket_status');
         define('TICKET_PRIORITY_TABLE',$prefix.'ticket_priority');
diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php
index 57df9e013..e89ceead6 100644
--- a/include/ajax.tickets.php
+++ b/include/ajax.tickets.php
@@ -418,6 +418,88 @@ class TicketsAjaxAPI extends AjaxController {
     }
 
 
+  function referrals($tid) {
+
+      return $this->refer($tid);
+
+  }
+
+function refer($tid, $target=null) {
+    global $thisstaff;
+
+    if (!($ticket=Ticket::lookup($tid)))
+        Http::response(404, __('No such ticket'));
+
+    if (!$ticket->checkStaffPerm($thisstaff, Ticket::PERM_ASSIGN)
+            || !($form = $ticket->getReferralForm($_POST,
+                    array('target' => $target))))
+        Http::response(403, __('Permission denied'));
+
+    $errors = array();
+    $info = array(
+            ':title' => sprintf(__('Ticket #%s: %s'),
+                $ticket->getNumber(),
+                __('Refer')
+                ),
+            ':action' => sprintf('#tickets/%d/refer%s',
+                $ticket->getId(),
+                ($target  ? "/$target": '')),
+            );
+
+    if ($_POST) {
+
+        switch ($_POST['do']) {
+        case 'refer':
+            if ($form->isValid() && $ticket->refer($form, $errors)) {
+                $_SESSION['::sysmsgs']['msg'] = sprintf(
+                        __('%s successfully'),
+                        sprintf(
+                            __('%s referred to %s'),
+                            sprintf(__('Ticket #%s'),
+                                 sprintf('<a href="tickets.php?id=%d"><b>%s</b></a>',
+                                     $ticket->getId(),
+                                     $ticket->getNumber()))
+                            ,
+                            $form->getTarget())
+                        );
+                Http::response(201, $ticket->getId());
+            }
+
+            $form->addErrors($errors);
+            $info['error'] = $errors['err'] ?: __('Unable to refer ticket');
+            break;
+        case 'manage':
+            $remove = array();
+            if (is_array($_POST['referrals'])) {
+                $remove = array();
+                foreach ($_POST['referrals'] as $k => $v)
+                    if ($v[0] == '-')
+                        $remove[] = substr($v, 1);
+                if (count($remove)) {
+                    $num = $ticket->thread->referrals
+                        ->filter(array('id__in' => $remove))
+                        ->delete();
+                    if ($num) {
+                        $info['msg'] = sprintf(
+                                __('%s successfully'),
+                                sprintf(__('Removed %d referrals'),
+                                    $num
+                                    )
+                                );
+                    }
+                    //TODO: log removal
+                }
+            }
+            break;
+        default:
+             $errors['err'] = __('Unknown Action');
+        }
+    }
+
+    $thread = $ticket->getThread();
+    include STAFFINC_DIR . 'templates/refer.tmpl.php';
+}
+
     function assign($tid, $target=null) {
         global $thisstaff;
 
@@ -545,6 +627,9 @@ class TicketsAjaxAPI extends AjaxController {
                 'assign' => array(
                     'verbed' => __('assigned'),
                     ),
+                'refer' => array(
+                    'verbed' => __('referred'),
+                    ),
                 'claim' => array(
                     'verbed' => __('assigned'),
                     ),
diff --git a/include/class.forms.php b/include/class.forms.php
index eaf5dbd25..6f13a4a0c 100644
--- a/include/class.forms.php
+++ b/include/class.forms.php
@@ -2671,17 +2671,24 @@ class AssigneeField extends ChoiceField {
     }
 
     function to_php($value, $id=false) {
+        $type = '';
         if (is_array($id)) {
             reset($id);
             $id = key($id);
+            $type = $id[0];
+            $id = substr($id, 1);
         }
 
-        if ($id[0] == 's')
-            return Staff::lookup(substr($id, 1));
-        elseif ($id[0] == 't')
-            return Team::lookup(substr($id, 1));
-
-        return $id;
+        switch ($type) {
+        case 's':
+            return Staff::lookup($id);
+        case 't':
+            return Team::lookup($id);
+        case 'd':
+            return Dept::lookup($id);
+        default:
+            return $id;
+        }
     }
 
 
@@ -4613,6 +4620,128 @@ class ClaimForm extends AssignmentForm {
 
 }
 
+class ReferralForm extends Form {
+
+    static $id = 'refer';
+    var $_target = null;
+    var $_choices = null;
+    var $_prompt = '';
+
+    function getFields() {
+
+        if ($this->fields)
+            return $this->fields;
+
+        $fields = array(
+            'target' => new AssigneeField(array(
+                    'id'=>1,
+                    'label' => __('Referee'),
+                    'flags' => hexdec(0X450F3),
+                    'required' => true,
+                    'validator-error' => __('Selection required'),
+                    'configuration' => array(
+                        'criteria' => array(
+                            'available' => true,
+                            ),
+                        'prompt' => $this->_prompt,
+                       ),
+                    )
+                ),
+            'comments' => new TextareaField(array(
+                    'id' => 2,
+                    'label'=> '',
+                    'required'=>false,
+                    'default'=>'',
+                    'configuration' => array(
+                        'html' => true,
+                        'size' => 'small',
+                        'placeholder' => __('Optional reason for the referral'),
+                        ),
+                    )
+                ),
+            );
+
+
+        if (isset($this->_choices))
+            $fields['target']->setChoices($this->_choices);
+
+
+        $this->setFields($fields);
+
+        return $this->fields;
+    }
+
+    function getField($name) {
+
+        if (($fields = $this->getFields())
+                && isset($fields[$name]))
+            return $fields[$name];
+    }
+
+    function isValid($include=false) {
+
+        if (!parent::isValid($include) || !($f=$this->getField('target')))
+            return false;
+
+        // Do additional assignment validation
+        $choice = $this->getTarget();
+        switch (true) {
+        case $choice instanceof Staff:
+            // Make sure the agent is available
+            if (!$choice->isAvailable())
+                $f->addError(__('Agent is unavailable for assignment'));
+        break;
+        case $choice instanceof Team:
+            // Make sure the team is active and has members
+            if (!$choice->isActive())
+                $f->addError(__('Team is disabled'));
+            elseif (!$choice->getNumMembers())
+                $f->addError(__('Team does not have members'));
+        break;
+        case $choice instanceof Dept:
+        break;
+        default:
+            $f->addError(__('Unknown selection'));
+        }
+
+        return !$this->errors();
+    }
+
+    function render($options) {
+
+        switch(strtolower($options['template'])) {
+        case 'simple':
+            $inc = STAFFINC_DIR . 'templates/dynamic-form-simple.tmpl.php';
+            break;
+        default:
+            throw new Exception(sprintf(__('%s: Unknown template style %s'),
+                        'FormUtils', $options['template']));
+        }
+
+        $form = $this;
+        include $inc;
+    }
+
+    function setChoices($choices, $prompt='') {
+        $this->_choices = $choices;
+        $this->_prompt = $prompt;
+        $this->_fields = array();
+    }
+
+    function getTarget() {
+
+        if (!isset($this->_target))
+            $this->_target = $this->getField('target')->getClean();
+
+        return $this->_target;
+    }
+
+    function getComments() {
+        return $this->getField('comments')->getClean();
+    }
+}
+
+
 class TransferForm extends Form {
 
     static $id = 'transfer';
diff --git a/include/class.model.php b/include/class.model.php
index 5d41a05d0..c9158a2e8 100644
--- a/include/class.model.php
+++ b/include/class.model.php
@@ -22,6 +22,9 @@ class ObjectModel {
     const OBJECT_TYPE_FAQ    = 'K';
     const OBJECT_TYPE_FILE   = 'F';
     const OBJECT_TYPE_TASK   = 'A';
+    const OBJECT_TYPE_TEAM   = 'E';
+    const OBJECT_TYPE_DEPT   = 'D';
+    const OBJECT_TYPE_STAFF  = 'S';
 
     private function objects() {
         static $objects = false;
@@ -34,6 +37,9 @@ class ObjectModel {
                     self::OBJECT_TYPE_FAQ     => 'FAQ',
                     self::OBJECT_TYPE_FILE    => 'AttachmentFile',
                     self::OBJECT_TYPE_TASK    => 'Task',
+                    self::OBJECT_TYPE_TEAM    => 'Team',
+                    self::OBJECT_TYPE_DEPT    => 'Dept',
+                    self::OBJECT_TYPE_STAFF   => 'Staff',
                     );
         }
 
diff --git a/include/class.staff.php b/include/class.staff.php
index c2c1b6767..bbe66e462 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -540,8 +540,34 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
 
         return $this->_teams;
     }
-    /* stats */
 
+    function getTicketsVisibility() {
+
+        // -- Open and assigned to me
+        $assigned = Q::any(array(
+            'staff_id' => $this->getId(),
+        ));
+
+        $assigned->add(array('thread__referrals__agent__staff_id' => $this->getId()));
+
+        // -- Open and assigned to a team of mine
+        if ($teams = array_filter($this->getTeams())) {
+            $assigned->add(array('team_id__in' => $teams));
+            $assigned->add(array('thread__referrals__team__team_id__in' => $teams));
+        }
+
+        $visibility = Q::any(new Q(array('status__state'=>'open', $assigned)));
+
+        // -- Routed to a department of mine
+        if (!$this->showAssignedOnly() && ($depts=$this->getDepts())) {
+            $visibility->add(array('dept_id__in' => $depts));
+            $visibility->add(array('thread__referrals__dept__id__in' => $depts));
+        }
+
+        return $visibility;
+    }
+
+    /* stats */
     function resetStats() {
         $this->stats = array();
     }
diff --git a/include/class.thread.php b/include/class.thread.php
index eab798690..bbb0d100b 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -40,6 +40,10 @@ implements Searchable {
             'collaborators' => array(
                 'reverse' => 'Collaborator.thread',
             ),
+
+            'referrals' => array(
+                'reverse' => 'ThreadReferral.thread',
+            ),
             'entries' => array(
                 'reverse' => 'ThreadEntry.thread',
             ),
@@ -104,6 +108,15 @@ implements Searchable {
         return $this->_entries;
     }
 
+    // Referrals
+    function getNumReferrals() {
+        return $this->referrals->count();
+    }
+
+    function getReferrals() {
+        return $this->referrals;
+    }
+
     // Collaborators
     function getNumCollaborators() {
         return $this->collaborators->count();
@@ -262,6 +275,30 @@ implements Searchable {
         return $this->_participants;
     }
 
+    function refer($to) {
+
+        $vars = array('thread_id' => $this->getId());
+        switch (true) {
+        case $to instanceof Staff:
+            $vars['object_id'] = $to->getId();
+            $vars['object_type'] = ObjectModel::OBJECT_TYPE_STAFF;
+            break;
+        case $to instanceof Team:
+            $vars['object_id'] = $to->getId();
+            $vars['object_type'] = ObjectModel::OBJECT_TYPE_TEAM;
+            break;
+        case $to instanceof Dept:
+            $vars['object_id'] = $to->getId();
+            $vars['object_type'] = ObjectModel::OBJECT_TYPE_DEPT;
+            break;
+        default:
+            return false;
+        }
+
+        var_dump($vars);
+
+        return ThreadReferral::create($vars);
+    }
 
     // Render thread
     function render($type=false, $options=array()) {
@@ -1636,6 +1673,79 @@ implements TemplateVariable {
 
 RolePermission::register(/* @trans */ 'Tickets', ThreadEntry::getPermissions());
 
+
+class ThreadReferral extends VerySimpleModel {
+    static $meta = array(
+        'table' => THREAD_REFERRAL_TABLE,
+        'pk' => array('id'),
+        'joins' => array(
+            'thread' => array(
+                'constraint' => array('thread_id' => 'Thread.id'),
+            ),
+            'agent' => array(
+                'constraint' => array(
+                    'object_type' => "'S'",
+                    'object_id' => 'Staff.staff_id',
+                ),
+            ),
+            'team' => array(
+                'constraint' => array(
+                    'object_type' => "'E'",
+                    'object_id' => 'Team.team_id',
+                ),
+            ),
+            'dept' => array(
+                'constraint' => array(
+                    'object_type' => "'D'",
+                    'object_id' => 'Dept.id',
+                ),
+            ),
+          )
+        );
+
+    var $icons = array(
+            'E' => 'group',
+            'D' => 'sitemap',
+            'S' => 'user'
+            );
+
+    var $_object = null;
+
+    function getId() {
+        return $this->id;
+    }
+
+    function getName() {
+        return (string) $this->getObject();
+    }
+
+    function getObject() {
+
+        if (!isset($this->_object)) {
+            $this->_object = ObjectModel::lookup(
+                    $this->object_id, $this->object_type);
+        }
+
+        return $this->_object;
+    }
+
+    function getIcon() {
+        return $this->icons[$this->object_type];
+    }
+
+    function display() {
+        return sprintf('<i class="icon-%s"></i> %s',
+                $this->getIcon(), $this->getName());
+    }
+
+    static function create($vars) {
+
+        $new = new self($vars);
+        $new->created = SqlFunction::NOW();
+        return $new->save();
+    }
+}
+
 class ThreadEvent extends VerySimpleModel {
     static $meta = array(
         'table' => THREAD_EVENT_TABLE,
@@ -1690,6 +1800,7 @@ class ThreadEvent extends VerySimpleModel {
     const REOPENED  = 'reopened';
     const STATUS    = 'status';
     const TRANSFERRED = 'transferred';
+    const REFERRED = 'referred';
     const VIEWED    = 'viewed';
 
     const MODE_STAFF = 1;
@@ -1719,6 +1830,7 @@ class ThreadEvent extends VerySimpleModel {
             'created'   => 'magic',
             'overdue'   => 'time',
             'transferred' => 'share-alt',
+            'referred' => 'exchange',
             'edited'    => 'pencil',
             'closed'    => 'thumbs-up-alt',
             'reopened'  => 'rotate-right',
@@ -1963,6 +2075,27 @@ class AssignmentEvent extends ThreadEvent {
     }
 }
 
+class ReferralEvent extends ThreadEvent {
+    static $icon = 'exchange';
+    static $state = 'referred';
+
+    function getDescription($mode=self::MODE_STAFF) {
+        $data = $this->getData();
+        switch (true) {
+        case isset($data['staff']):
+            $desc = __('<b>{somebody}</b> referred this to <strong>{<Staff>data.staff}</strong> {timestamp}');
+            break;
+        case isset($data['team']):
+            $desc = __('<b>{somebody}</b> referred this to <strong>{<Team>data.team}</strong> {timestamp}');
+            break;
+        case isset($data['dept']):
+            $desc = __('<b>{somebody}</b> referred this to <strong>{<Dept>data.dept}</strong> {timestamp}');
+            break;
+        }
+        return $this->template($desc);
+    }
+}
+
 class CloseEvent extends ThreadEvent {
     static $icon = 'thumbs-up-alt';
     static $state = 'closed';
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 332f10c12..f91ab94bf 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -852,6 +852,36 @@ implements RestrictedAccess, Threadable, Searchable {
         return $form;
     }
 
+    function getReferralForm($source=null, $options=array()) {
+
+        $prompt = '';
+        $choices = array();
+        switch (strtolower($options['target'])) {
+        case 'agents':
+            $dept = $this->getDept();
+            foreach ($dept->getAssignees() as $member)
+                $choices['s'.$member->getId()] = $member;
+            $prompt = sprintf('%s %s', __('Select an'), __('Agent'));
+            break;
+        case 'teams':
+            if (($teams = Team::getActiveTeams()))
+                foreach ($teams as $id => $name)
+                    $choices['t'.$id] = $name;
+            $prompt = sprintf('%s %s', __('Select a'), __('Team'));
+            break;
+        case 'departments':
+            foreach (Dept::getDepartments() as $k => $v)
+                $choices["d$k"] = $v;
+            $prompt = sprintf('%s %s', __('Select a'), __('Department'));
+            break;
+        }
+
+        $form = ReferralForm::instantiate($source, $options);
+        $form->setChoices($choices, $prompt);
+
+        return $form;
+    }
+
     function getClaimForm($source=null, $options=array()) {
         global $thisstaff;
 
@@ -2283,6 +2313,65 @@ implements RestrictedAccess, Threadable, Searchable {
         return $this->unassign();
     }
 
+    function refer(ReferralForm $form, &$errors, $alert=true) {
+        global $thisstaff;
+
+        $evd = array();
+        $choice = $form->getTarget();
+        switch (true) {
+        case $choice instanceof Staff:
+            $dept = $this->getDept();
+            if ($this->getStaffId() == $choice->getId()) {
+                $errors['target'] = sprintf(__('%s is assigned to %s'),
+                        __('Ticket'),
+                        __('the agent')
+                        );
+            } elseif(!$choice->isAvailable()) {
+                $errors['assignee'] = sprintf(__('Agent is unavailable for %s'),
+                        __('referral'));
+            } elseif ($dept->assignMembersOnly() && !$dept->isMember($choice)) {
+                $errors['err'] = __('Permission denied');
+            } else {
+                $evd['staff'] = array($choice->getId(), (string) $choice->getName()->getOriginal());
+            }
+            break;
+        case $choice instanceof Team:
+            if ($this->getTeamId() == $choice->getId()) {
+                $errors['assignee'] = sprintf(__('%s is assigned to %s'),
+                        __('Ticket'),
+                        __('the team')
+                        );
+            } else {
+                //TODO::
+                $evd = array('team' => $choice->getId());
+            }
+            break;
+        case $choice instanceof Dept:
+            if ($this->getTeamId() == $choice->getId()) {
+                $errors['target'] = sprintf(__('%s is already in %s'),
+                        __('Ticket'),
+                        __('the department')
+                        );
+            } else {
+                //TODO::
+                $evd = array('dept' => $choice->getId());
+            }
+            break;
+        default:
+            $errors['target'] = __('Unknown referral');
+        }
+
+        if (!$errors && !$this->thread->refer($choice))
+            $errors['err'] = __('Unable to reffer ticket');
+
+        if ($errors)
+            return false;
+
+        $this->logEvent('referred', $evd);
+
+        return true;
+    }
+
     //Change ownership
     function changeOwner($user) {
         global $thisstaff;
@@ -3073,19 +3162,7 @@ implements RestrictedAccess, Threadable, Searchable {
         if(!$staff || (!is_object($staff) && !($staff=Staff::lookup($staff))) || !$staff->isStaff())
             return null;
 
-        // -- Open and assigned to me
-        $assigned = Q::any(array(
-            'staff_id' => $staff->getId(),
-        ));
-        // -- Open and assigned to a team of mine
-        if ($teams = array_filter($staff->getTeams()))
-            $assigned->add(array('team_id__in' => $teams));
-
-        $visibility = Q::any(new Q(array('status__state'=>'open', $assigned)));
-
-        // -- Routed to a department of mine
-        if (!$staff->showAssignedOnly() && ($depts = $staff->getDepts()))
-            $visibility->add(array('dept_id__in' => $depts));
+        $visibility = $staff->getTicketsVisibility();
 
         $blocks = Ticket::objects()
             ->filter(Q::any($visibility))
diff --git a/include/staff/templates/refer.tmpl.php b/include/staff/templates/refer.tmpl.php
new file mode 100644
index 000000000..b1c850f8a
--- /dev/null
+++ b/include/staff/templates/refer.tmpl.php
@@ -0,0 +1,129 @@
+<?php
+global $cfg;
+
+if (!$thread) return;
+
+$form = $form ?: ReferralForm::instantiate($info);
+
+?>
+<h3 class="drag-handle"><?php echo $info[':title'] ?:  __('Refer'); ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<div class="clear"></div>
+<hr/>
+<?php
+if ($info['error']) {
+    echo sprintf('<p id="msg_error">%s</p>', $info['error']);
+} elseif ($info['warn']) {
+    echo sprintf('<p id="msg_warning">%s</p>', $info['warn']);
+} elseif ($info['msg']) {
+    echo sprintf('<p id="msg_notice">%s</p>', $info['msg']);
+} elseif ($info['notice']) {
+   echo sprintf('<p id="msg_info"><i class="icon-info-sign"></i> %s</p>',
+           $info['notice']);
+}
+
+$action = $info[':action'] ?: ('#');
+$manage = (!$target);
+?>
+<ul class="tabs" id="referral">
+    <li <?php echo !$manage ? 'class="active"' : ''; ?>><a href="#refer"
+        ><i class="icon-exchange"></i>&nbsp;<?php echo __('Refer'); ?></a></li>
+    <li <?php echo $manage ? 'class="active"' : ''; ?>><a href="#referrals"
+        ><i class="icon-list"></i>&nbsp;<?php
+        echo sprintf('%s (%d)', __('Referrals'), $thread->getNumReferrals()); ?></a></li>
+</ul>
+<div id="referral_container">
+   <div class="tab_content <?php echo $manage ? 'hidden' : ''; ?>" id="refer" style="margin:5px;">
+    <form class="mass-action" method="post"
+        name="assign"
+        id="<?php echo $form->getId(); ?>"
+        action="<?php echo $action; ?>">
+      <input type='hidden' name='do' value='refer'>
+    <table width="100%">
+        <?php
+        if ($info[':extra']) {
+            ?>
+        <tbody>
+            <tr><td colspan="2"><strong><?php echo $info[':extra'];
+            ?></strong></td> </tr>
+        </tbody>
+        <?php
+        }
+       ?>
+        <tbody>
+            <tr><td colspan=2>
+             <?php
+             $options = array('template' => 'simple', 'form_id' => 'refer');
+             $form->render($options);
+             ?>
+            </td> </tr>
+        </tbody>
+    </table>
+    <hr>
+    <p class="full-width">
+        <span class="buttons pull-left">
+            <input type="reset" value="<?php echo __('Reset'); ?>">
+            <input type="button" name="cancel" class="close"
+            value="<?php echo __('Cancel'); ?>">
+        </span>
+        <span class="buttons pull-right">
+            <input type="submit" value="<?php
+            echo __('Refer'); ?>">
+        </span>
+     </p>
+    </form>
+    </div>
+   <div class="tab_content <?php echo !$manage ? 'hidden' : ''; ?>" id="referrals" style="margin:5px;">
+   <form class="mass-action" method="post"
+    name="referrals"
+    id="rf"
+    action="<?php echo sprintf('#/tickets/%d/referrals', $ticket->getId()); ?>">
+     <input type='hidden' name='do' value='manage'>
+    <table width="100%">
+        <tbody>
+           <?php
+           if ($thread->referrals->count()) {
+            foreach ($thread->referrals as $r) {
+            ?>
+            <tr>
+                <td style="border-top: 1px solid #ddd;"> <?php echo  $r->display(); ?></td>
+                <td style="border-top: 1px solid #ddd;">
+                    <div style="position:relative">
+                    <input type="hidden" name="referrals[]" value="<?php echo $r->getId(); ?>"/>
+                    <div class="pull-right" style="right:2px;">
+                        <a href="#" title="<?php echo __('clear'); ?>" onclick="javascript:
+                            if (!confirm(__('You sure?')))
+                            return false;
+                            $(this).closest('td').find('input[name=\'referrals[]\']')
+                                .val(function(i,v) { return '-'+ v; });
+                            $(this).closest('tr').fadeOut(400, function() { $(this).hide(); });
+                            $('input[type=submit], button[type=submit]',
+                                    $(this).closest('form')).addClass('save pending');
+                            return false;"><i class="icon-trash"></i></a>
+                        </div>
+                    </div>
+                </td>
+            </tr>
+            <?php }
+            } ?>
+        </tbody>
+    </table>
+    <hr>
+    <?php
+    if ($thread->getNumReferrals()) {?>
+    <p class="full-width">
+        <span class="buttons pull-left">
+            <input type="button" name="cancel" class="close"
+            value="<?php echo __('Cancel'); ?>">
+        </span>
+        <span class="buttons pull-right">
+            <input type="submit" value="<?php
+            echo __('Save Changes'); ?>">
+        </span>
+     </p>
+     <?php
+    } ?>
+    </form>
+  </div>
+</div>
+<div class="clear"></div>
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index a977ab2c6..81965fbce 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -92,6 +92,33 @@ if($ticket->isOverdue())
                 data-redirect="tickets.php"
                 href="#tickets/<?php echo $ticket->getId(); ?>/transfer"><i class="icon-share"></i></a>
             </span>
+            <span class="action-button pull-right"
+                data-dropdown="#action-dropdown-refer"
+                data-placement="bottom"
+                data-toggle="tooltip"
+                title=" <?php echo __('Refer'); ?>"
+                >
+                <i class="icon-caret-down pull-right"></i>
+                <a class="ticket-action" id="ticket-refer"
+                    data-redirect="tickets.php"
+                    href="#tickets/<?php echo $ticket->getId(); ?>/refer"><i class="icon-exchange"></i></a>
+            </span>
+            <div id="action-dropdown-refer" class="action-dropdown anchor-right">
+              <ul>
+                 <li><a class="no-pjax ticket-action"
+                    data-redirect="tickets.php"
+                    href="#tickets/<?php echo $ticket->getId(); ?>/refer/agents"><i
+                    class="icon-user"></i> <?php echo __('Agent'); ?></a>
+                 <li><a class="no-pjax ticket-action"
+                    data-redirect="tickets.php"
+                    href="#tickets/<?php echo $ticket->getId(); ?>/refer/teams"><i
+                    class="icon-group"></i> <?php echo __('Team'); ?></a>
+                 <li><a class="no-pjax ticket-action"
+                    data-redirect="tickets.php"
+                    href="#tickets/<?php echo $ticket->getId(); ?>/refer/departments"><i
+                    class="icon-sitemap"></i> <?php echo __('Department'); ?></a>
+              </ul>
+            </div>
             <?php
             } ?>
 
diff --git a/scp/ajax.php b/scp/ajax.php
index 215079e18..b935414c1 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -165,6 +165,8 @@ $dispatcher = patterns('',
         url('^mass/(?P<action>\w+)(?:/(?P<what>\w+))?', 'massProcess'),
         url('^(?P<tid>\d+)/transfer$', 'transfer'),
         url('^(?P<tid>\d+)/assign(?:/(?P<to>\w+))?$', 'assign'),
+        url('^(?P<tid>\d+)/refer(?:/(?P<to>\w+))?$', 'refer'),
+        url('^(?P<tid>\d+)/referrals$', 'referrals'),
         url('^(?P<tid>\d+)/claim$', 'claim'),
         url('^search', patterns('ajax.search.php:SearchAjaxAPI',
             url_get('^$', 'getAdvancedSearchDialog'),
-- 
GitLab