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> <?php echo __('Refer'); ?></a></li> + <li <?php echo $manage ? 'class="active"' : ''; ?>><a href="#referrals" + ><i class="icon-list"></i> <?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