From d354e095f7cba226c83f62a884bfe59799774fa6 Mon Sep 17 00:00:00 2001 From: JediKev <kevin@enhancesoft.com> Date: Tue, 12 Jun 2018 14:22:43 -0500 Subject: [PATCH] feature: Release Role Permission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This feature adds a new Role permission called Release. This permission (if Enabled) allows an Agent to release ticket assignment. This permission is configurable for each and every Role in the helpdesk. This also keeps current functionality where Department Managers do not need the Role Permission in order to release tickets. In addition to the permission, this feature adds a new Release modal giving the option to choose who to release assignment from (Agent/Team/Both). This also adds a comment box to the release modal to optionally enter a reason for releasing assignment. Lastly, this adds a Release Thread Event so an event is logged showing who released who’s assignment. --- include/ajax.tickets.php | 60 ++++++++++++++++ include/class.forms.php | 43 ++++++++++++ include/class.thread.php | 28 +++++++- include/class.ticket.php | 17 ++++- include/i18n/en_US/role.yaml | 2 + include/staff/templates/release.tmpl.php | 89 ++++++++++++++++++++++++ include/staff/ticket-view.inc.php | 18 ++--- scp/ajax.php | 1 + scp/tickets.php | 13 ---- 9 files changed, 247 insertions(+), 24 deletions(-) create mode 100644 include/staff/templates/release.tmpl.php diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 643d8b461..05cf5354c 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -652,6 +652,66 @@ function refer($tid, $target=null) { } + function release($tid) { + global $thisstaff; + + if (!($ticket=Ticket::lookup($tid))) + Http::response(404, __('No such ticket')); + + if (!$ticket->checkStaffPerm($thisstaff, Ticket::PERM_RELEASE) && !$thisstaff->isManager()) + Http::response(403, __('Permission denied')); + + if (!$ticket->isAssigned()) + $errors['err'] = __('Ticket is not assigned!'); + + + $errors = array(); + $info = array(':title' => sprintf(__('Ticket #%s: %s'), + $ticket->getNumber(), + __('Release Confirmation'))); + + $form = ReleaseForm::instantiate($_POST); + $hasData = ($_POST['sid'] || $_POST['tid']); + + $staff = $ticket->getStaff(); + $team = $ticket->getTeam(); + if ($_POST) { + if ($hasData && $ticket->release($_POST, $errors)) { + $data = array(); + + if ($staff && !$ticket->getStaff()) + $data['staff'] = array($staff->getId(), (string) $staff->getName()->getOriginal()); + if ($team && !$ticket->getTeam()) + $data['team'] = $team->getId(); + $ticket->logEvent('released', $data); + + $comments = $form->getComments(); + if ($comments) { + $title = __('Assignment Released'); + $_errors = array(); + + $ticket->postNote( + array('note' => $comments, 'title' => $title), + $_errors, $thisstaff, false); + } + + $_SESSION['::sysmsgs']['msg'] = __('Ticket assignment released successfully'); + Http::response(201, $ticket->getId()); + } + + if (!$hasData) + $errors['err'] = __('Please check an assignee to release assignment'); + + $form->addErrors($errors); + $info['error'] = $errors['err'] ?: __('Unable to release ticket assignment'); + } + + if($errors && $errors['err']) + $info['error'] = $errors['err'] ?: __('Unable to release ticket'); + + include STAFFINC_DIR . 'templates/release.tmpl.php'; + } + function massProcess($action, $w=null) { global $thisstaff, $cfg; diff --git a/include/class.forms.php b/include/class.forms.php index e13f5f15d..6f28d71f9 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -4928,6 +4928,49 @@ class ClaimForm extends AssignmentForm { } +class ReleaseForm extends Form { + static $id = 'unassign'; + + function getFields() { + if ($this->fields) + return $this->fields; + + $fields = array( + 'comments' => new TextareaField(array( + 'id' => 1, 'label'=> '', 'required'=>false, 'default'=>'', + 'configuration' => array( + 'html' => true, + 'size' => 'small', + 'placeholder' => __('Optional reason for releasing assignment'), + ), + ) + ), + ); + + + $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)) + return false; + + return !$this->errors(); + } + + function getComments() { + return $this->getField('comments')->getClean(); + } +} + class ReferralForm extends Form { static $id = 'refer'; diff --git a/include/class.thread.php b/include/class.thread.php index 3393043e6..7cca14af8 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -1871,6 +1871,7 @@ class ThreadEvent extends VerySimpleModel { // Valid events for database storage const ASSIGNED = 'assigned'; + const RELEASED = 'released'; const CLOSED = 'closed'; const CREATED = 'created'; const COLLAB = 'collab'; @@ -1906,6 +1907,7 @@ class ThreadEvent extends VerySimpleModel { function getIcon() { $icons = array( 'assigned' => 'hand-right', + 'released' => 'unlock', 'collab' => 'group', 'created' => 'magic', 'overdue' => 'time', @@ -2092,7 +2094,7 @@ class ThreadEvents extends InstrumentedList { * $object - Object to log activity for * $state - State name of the activity (one of 'created', 'edited', * 'deleted', 'closed', 'reopened', 'error', 'collab', 'resent', - * 'assigned', 'transferred') + * 'assigned', 'released', 'transferred') * $data - (array?) Details about the state change * $user - (string|User|Staff) user triggering the state change * $annul - (state) a corresponding state change that is annulled by @@ -2175,6 +2177,30 @@ class AssignmentEvent extends ThreadEvent { } } +class ReleaseEvent extends ThreadEvent { + static $icon = 'unlock'; + static $state = 'released'; + + function getDescription($mode=self::MODE_STAFF) { + $data = $this->getData(); + switch (true) { + case isset($data['staff'], $data['team']): + $desc = __('Ticket released from <strong>{<Team>data.team}</strong> and <strong>{<Staff>data.staff}</strong> by <b>{somebody}</b> {timestamp}'); + break; + case isset($data['staff']): + $desc = __('Ticket released from <strong>{<Staff>data.staff}</strong> by <b>{somebody}</b> {timestamp}'); + break; + case isset($data['team']): + $desc = __('Ticket released from <strong>{<Team>data.team}</strong> by <b>{somebody}</b> {timestamp}'); + break; + default: + $desc = __('<b>{somebody}</b> released ticket assignment {timestamp}'); + break; + } + return $this->template($desc); + } +} + class ReferralEvent extends ThreadEvent { static $icon = 'exchange'; static $state = 'referred'; diff --git a/include/class.ticket.php b/include/class.ticket.php index 5078b566e..160c5630e 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -98,6 +98,7 @@ implements RestrictedAccess, Threadable, Searchable { const PERM_CREATE = 'ticket.create'; const PERM_EDIT = 'ticket.edit'; const PERM_ASSIGN = 'ticket.assign'; + const PERM_RELEASE = 'ticket.release'; const PERM_TRANSFER = 'ticket.transfer'; const PERM_REPLY = 'ticket.reply'; const PERM_CLOSE = 'ticket.close'; @@ -119,6 +120,11 @@ implements RestrictedAccess, Threadable, Searchable { /* @trans */ 'Assign', 'desc' => /* @trans */ 'Ability to assign tickets to agents or teams'), + self::PERM_RELEASE => array( + 'title' => + /* @trans */ 'Release', + 'desc' => + /* @trans */ 'Ability to release ticket assignment'), self::PERM_TRANSFER => array( 'title' => /* @trans */ 'Transfer', @@ -2470,8 +2476,15 @@ implements RestrictedAccess, Threadable, Searchable { return true; } - function release() { - return $this->unassign(); + function release($info=array(), &$errors) { + if ($info['sid'] && $info['tid']) + return $this->unassign(); + elseif ($info['sid'] && $this->setStaffId(0)) + return true; + elseif ($info['tid'] && $this->setTeamId(0)) + return true; + + return false; } function refer(ReferralForm $form, &$errors, $alert=true) { diff --git a/include/i18n/en_US/role.yaml b/include/i18n/en_US/role.yaml index ca76b2650..9688cae1f 100644 --- a/include/i18n/en_US/role.yaml +++ b/include/i18n/en_US/role.yaml @@ -20,6 +20,7 @@ ticket.create, ticket.edit, ticket.assign, + ticket.release, ticket.transfer, ticket.reply, ticket.close, @@ -44,6 +45,7 @@ ticket.create, ticket.edit, ticket.assign, + ticket.release, ticket.transfer, ticket.reply, ticket.close, diff --git a/include/staff/templates/release.tmpl.php b/include/staff/templates/release.tmpl.php new file mode 100644 index 000000000..a60fe50a5 --- /dev/null +++ b/include/staff/templates/release.tmpl.php @@ -0,0 +1,89 @@ +<?php +global $cfg; + +$assignees = array(); +if (($staff = $ticket->getStaff())) + $assignees[] = $staff; +if (($team = $ticket->getTeam())) + $assignees[] = $team; + +$form = ReleaseForm::instantiate($_POST); +?> +<h3 class="drag-handle"><?php echo $info[':title'] ?: __('Release Confirmation'); ?></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']); +} +?> +<form class="mass-action" method="post" + action="#tickets/<?php echo $ticket->getId(); ?>/release" + name="release"> + <input type='hidden' name='do' value='release'> + <table width="100%"> + <tbody> + <?php if ($staff && $team) { ?> + <tr><td> + <p> + <?php echo __('Please check assignee(s) to release assignment.'); ?> + </p> + </td></tr> + <?php } ?> + <?php if(count($assignees) > 1) { ?> + <?php foreach($assignees as $assignee) { ?> + <tr><td> + <label class="inline checkbox"> + <?php echo sprintf( + ($isStaff = $assignee instanceof Staff) + ? '<input type="checkbox" name="sid[]" id="s%d" value="%d">' + : '<input type="checkbox" name="tid[]" id="t%d" value="%d">', + $assignee->getId(), + $assignee->getId()); ?> + </label> + <?php echo '<i class="icon-'.(($isStaff) ? 'user' : 'group').'"></i>'; ?> + <?php echo $assignee->getName(); ?> + </td></tr> + <?php } ?> + <?php } else { ?> + <tr><td> + <input type="hidden" name="<?php echo (($staff)?'s':'t').'id[]'; ?>" value="()"> + <p> + <?php echo __('Please confirm to continue.'); ?> + </p> + <p> + <?php echo sprintf( + __('Are you sure you want to <b>unassign</b> ticket from <b>%s</b>?'), + ($staff) ?: $team); ?> + </p> + </td></tr> + <?php } ?> + <tr><td> + <p> + <?php print $form->getField('comments')->render(); ?> + </p> + </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 __('Release'); ?>"> + </span> + </p> +</form> +<div class="clear"></div> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 21d5646e9..33272a222 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -20,6 +20,8 @@ if (!$lock && $cfg->getTicketLockMode() == Lock::MODE_ON_VIEW) $lock = $ticket->acquireLock($thisstaff->getId()); $mylock = ($lock && $lock->getStaffId() == $thisstaff->getId()) ? $lock : null; $id = $ticket->getId(); //Ticket ID. +$isManager = $dept->isManager($thisstaff); //Check if Agent is Manager +$canRelease = ($isManager || $role->hasPerm(Ticket::PERM_RELEASE)); //Check if Agent can release tickets //Useful warnings and errors the user might want to know! if ($ticket->isClosed() && !$ticket->isReopenable()) @@ -146,14 +148,14 @@ if($ticket->isOverdue()) <?php } - if($ticket->isOpen() && ($dept && $dept->isManager($thisstaff))) { - - if($ticket->isAssigned()) { ?> - <li><a class="confirm-action" id="ticket-release" href="#release"><i class="icon-user"></i> <?php - echo __('Release (unassign) Ticket'); ?></a></li> - <?php - } - + if ($ticket->isAssigned() && $canRelease) { ?> + <li><a href="#tickets/<?php echo $ticket->getId(); + ?>/release" class="ticket-action" + data-redirect="tickets.php?id=<?php echo $ticket->getId(); ?>" > + <i class="icon-unlock"></i> <?php echo __('Release (unassign) Ticket'); ?></a></li> + <?php + } + if($ticket->isOpen() && $isManager) { if(!$ticket->isOverdue()) { ?> <li><a class="confirm-action" id="ticket-overdue" href="#overdue"><i class="icon-bell"></i> <?php echo __('Mark as Overdue'); ?></a></li> diff --git a/scp/ajax.php b/scp/ajax.php index 7dc4a56a2..dfdb7499e 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -167,6 +167,7 @@ $dispatcher = patterns('', url('^(?P<tid>\d+)/field/(?P<fid>\d+)/edit$', 'editField'), url('^(?P<tid>\d+)/field/(?P<field>\w+)/edit$', 'editField'), url('^(?P<tid>\d+)/assign(?:/(?P<to>\w+))?$', 'assign'), + url('^(?P<tid>\d+)/release$', 'release'), url('^(?P<tid>\d+)/refer(?:/(?P<to>\w+))?$', 'refer'), url('^(?P<tid>\d+)/referrals$', 'referrals'), url('^(?P<tid>\d+)/claim$', 'claim'), diff --git a/scp/tickets.php b/scp/tickets.php index 1e1a085ab..ca0770817 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -282,19 +282,6 @@ if($_POST && !$errors): break; case 'process': switch(strtolower($_POST['do'])): - case 'release': - if(!$ticket->isAssigned() || !($assigned=$ticket->getAssigned())) { - $errors['err'] = __('Ticket is not assigned!'); - } elseif($ticket->release()) { - $msg=sprintf(__( - /* 1$ is the current assignee, 2$ is the agent removing the assignment */ - 'Ticket released (unassigned) from %1$s by %2$s'), - $assigned, $thisstaff->getName()); - $ticket->logActivity(__('Ticket unassigned'),$msg); - } else { - $errors['err'] = sprintf('%s %s', __('Problems releasing the ticket.'), __('Please try again!')); - } - break; case 'claim': if(!$role->hasPerm(Ticket::PERM_EDIT)) { $errors['err'] = __('Permission Denied. You are not allowed to assign/claim tickets.'); -- GitLab