diff --git a/include/ajax.tasks.php b/include/ajax.tasks.php index e0330dc98daf11ec850e10a6ac859c4916c67271..31ce73c4835f1f8384c5fc9f6cc6f4f59459519e 100644 --- a/include/ajax.tasks.php +++ b/include/ajax.tasks.php @@ -22,6 +22,41 @@ require_once(INCLUDE_DIR.'class.task.php'); class TasksAjaxAPI extends AjaxController { + function lookup() { + global $thisstaff; + + $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25; + $tasks = array(); + + $visibility = Q::any(array( + 'staff_id' => $thisstaff->getId(), + 'team_id__in' => $thisstaff->teams->values_flat('team_id'), + )); + + if (!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts())) { + $visibility->add(array('dept_id__in' => $depts)); + } + + + $hits = TaskModel::objects() + ->filter(Q::any(array( + 'number__startswith' => $_REQUEST['q'], + ))) + ->filter($visibility) + ->values('number') + ->annotate(array('tasks' => SqlAggregate::COUNT('id'))) + ->order_by('-created') + ->limit($limit); + + foreach ($hits as $T) { + $tasks[] = array('id'=>$T['number'], 'value'=>$T['number'], + 'info'=>"{$T['number']}", + 'matches'=>$_REQUEST['q']); + } + + return $this->json_encode($tasks); + } + function add() { global $thisstaff; @@ -86,9 +121,16 @@ class TasksAjaxAPI extends AjaxController { $forms = DynamicFormEntry::forObject($task->getId(), ObjectModel::OBJECT_TYPE_TASK); - if ($_POST) { + if ($_POST && $forms) { + // TODO: Validate internal form + + // Validate dynamic meta-data + if ($task->update($forms, $_POST, $errors)) { + Http::response(201, 'Task updated successfully'); + } elseif(!$errors['err']) { + $errors['err']=__('Unable to update the task. Correct the errors below and try again!'); + } $info = Format::htmlchars($_POST); - $info['error'] = $errors['err'] ?: __('Coming soon!'); } include STAFFINC_DIR . 'templates/task-edit.tmpl.php'; @@ -198,7 +240,7 @@ class TasksAjaxAPI extends AjaxController { } // Check generic permissions -- department specific permissions // will be checked below. - if ($perm && !$thisstaff->hasPerm($perm)) + if ($perm && !$thisstaff->hasPerm($perm, false)) $errors['err'] = sprintf( __('You do not have permission to %s %s'), __($action), diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index dde62c7f2a511474610c5913d14446c6df87049d..6b1c6cd6e3ebe471ba32d3a9d803596e6a3407a1 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -793,32 +793,54 @@ class TicketsAjaxAPI extends AjaxController { || !$task->checkStaffPerm($thisstaff)) Http::response(404, 'Unknown task'); - $info=$errors=array(); - $note_form = new SimpleForm(array( + $info = $errors = array(); + $note_attachments_form = new SimpleForm(array( 'attachments' => new FileUploadField(array('id'=>'attach', - 'name'=>'attach:note', - 'configuration' => array('extensions'=>''))) - )); + 'name'=>'attach:note', + 'configuration' => array('extensions'=>''))) + )); + + $reply_attachments_form = new SimpleForm(array( + 'attachments' => new FileUploadField(array('id'=>'attach', + 'name'=>'attach:reply', + 'configuration' => array('extensions'=>''))) + )); if ($_POST) { + $vars = $_POST; switch ($_POST['a']) { case 'postnote': - $vars = $_POST; - $attachments = $note_form->getField('attachments')->getClean(); + $attachments = $note_attachments_form->getField('attachments')->getClean(); $vars['cannedattachments'] = array_merge( $vars['cannedattachments'] ?: array(), $attachments); - if(($note=$task->postNote($vars, $errors, $thisstaff))) { + if (($note=$task->postNote($vars, $errors, $thisstaff))) { $msg=__('Note posted successfully'); // Clear attachment list - $note_form->setSource(array()); - $note_form->getField('attachments')->reset(); + $note_attachments_form->setSource(array()); + $note_attachments_form->getField('attachments')->reset(); Draft::deleteForNamespace('task.note.'.$task->getId(), $thisstaff->getId()); } else { - if(!$errors['err']) + if (!$errors['err']) $errors['err'] = __('Unable to post the note - missing or invalid data.'); } break; + case 'postreply': + $attachments = $reply_attachments_form->getField('attachments')->getClean(); + $vars['cannedattachments'] = array_merge( + $vars['cannedattachments'] ?: array(), $attachments); + if (($response=$task->postReply($vars, $errors))) { + $msg=__('Update posted successfully'); + // Clear attachment list + $reply_attachments_form->setSource(array()); + $reply_attachments_form->getField('attachments')->reset(); + Draft::deleteForNamespace('task.reply.'.$task->getId(), + $thisstaff->getId()); + } else { + if (!$errors['err']) + $errors['err'] = __('Unable to post the reply - missing or invalid data.'); + } + break; default: $errors['err'] = __('Unknown action'); } diff --git a/include/class.config.php b/include/class.config.php index f9aec5d0f040a3bcf1e338a35dcdaece50ea586c..c9125aaa3a4be615079695d4d3668c43e35c911b 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -1181,7 +1181,6 @@ class OsticketConfig extends Config { function updateTasksSettings($vars, &$errors) { $f=array(); - $f['default_task_sla_id']=array('type'=>'int', 'required'=>1, 'error'=>__('Selection required')); $f['default_task_priority_id']=array('type'=>'int', 'required'=>1, 'error'=>__('Selection required')); if (!preg_match('`(?!<\\\)#`', $vars['task_number_format'])) diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php index c6dbac5f979e4c40393ad435b45ef297bf9a2362..479e94736fd3c59e263e0ab017ad968837e94589 100644 --- a/include/class.dynamic_forms.php +++ b/include/class.dynamic_forms.php @@ -616,8 +616,8 @@ class DynamicFormField extends VerySimpleModel { const FLAG_MASK_VIEW = 0x20000; const FLAG_MASK_NAME = 0x40000; - const MASK_MASK_INTERNAL = 0x400B0; # !change, !delete, !disable, !edit-name - const MASK_MASK_ALL = 0x700F0; + const MASK_MASK_INTERNAL = 0x400B2; # !change, !delete, !disable, !edit-name + const MASK_MASK_ALL = 0x700F2; const FLAG_CLIENT_VIEW = 0x00100; const FLAG_CLIENT_EDIT = 0x00200; @@ -1021,9 +1021,9 @@ class DynamicFormEntry extends VerySimpleModel { return $this->form; } - function getForm() { + function getForm($source=false, $options=array()) { if (!isset($this->_form)) { - // XXX: Should source be $this? + $fields = $this->getFields(); if (isset($this->extra)) { $x = JsonDataParser::decode($this->extra) ?: array(); @@ -1031,13 +1031,16 @@ class DynamicFormEntry extends VerySimpleModel { unset($fields[$id]); } } - $form = new SimpleForm($fields, $this->getSource(), - array( + + $source = $source ?: $this->getSource(); + $options += array( 'title' => $this->getTitle(), - 'instructions' => $this->getInstructions(), - )); - $this->_form = $form; + 'instructions' => $this->getInstructions() + ); + $this->_form = new CustomForm($fields, $source, $options); } + + return $this->_form; } @@ -1056,8 +1059,6 @@ class DynamicFormEntry extends VerySimpleModel { // even when stored elsewhere -- important during validation foreach ($this->getDynamicFields() as $f) { $f = $f->getImpl($f); - if ($f instanceof ThreadEntryField) - continue; $this->_fields[$f->get('id')] = $f; $f->isnew = true; } @@ -1103,16 +1104,17 @@ class DynamicFormEntry extends VerySimpleModel { * Parameters: * $filter - (callback) function to receive each field and return * boolean true if the field's errors are significant + * $options - options to pass to form and fields. + * */ - function isValid($filter=false) { + function isValid($filter=false, $options=array()) { + if (!is_array($this->_errors)) { - $this->_errors = array(); - $this->getClean(); - foreach ($this->getFields() as $field) { - if ($field->errors() && (!$filter || $filter($field))) - $this->_errors[$field->get('id')] = $field->errors(); - } + $form = $this->getForm(false, $options); + $form->isValid($filter); + $this->_errors = $form->errors(); } + return !$this->_errors; } diff --git a/include/class.export.php b/include/class.export.php index d12dd3bf4762ca9456c56ae31a270bf249264549..b6b9449a5fca4a7bcacb87121c873cc906f99cce 100644 --- a/include/class.export.php +++ b/include/class.export.php @@ -99,7 +99,7 @@ class Export { ); } - /* static */ function saveTickets($sql, $filename, $how='csv') { + static function saveTickets($sql, $filename, $how='csv') { ob_start(); self::dumpTickets($sql, $how); $stuff = ob_get_contents(); @@ -110,7 +110,64 @@ class Export { return false; } + + static function dumpTasks($sql, $how='csv') { + // Add custom fields to the $sql statement + $cdata = $fields = array(); + foreach (TaskForm::getInstance()->getFields() as $f) { + // Ignore non-data fields + if (!$f->hasData() || $f->isPresentationOnly()) + continue; + + $name = $f->get('name') ?: 'field_'.$f->get('id'); + $key = 'cdata.'.$name; + $fields[$key] = $f; + $cdata[$key] = $f->getLocal('label'); + } + // Reset the $sql query + $tasks = $sql->models() + ->select_related('dept', 'staff', 'team', 'cdata') + ->annotate(array( + 'collab_count' => SqlAggregate::COUNT('thread__collaborators'), + 'attachment_count' => SqlAggregate::COUNT('thread__entries__attachments'), + 'thread_count' => SqlAggregate::COUNT('thread__entries'), + )); + + return self::dumpQuery($tasks, + array( + 'number' => __('Task Number'), + 'created' => __('Date Created'), + 'cdata.title' => __('Title'), + 'dept::getLocalName' => __('Department'), + '::getStatus' => __('Current Status'), + 'duedate' => __('Due Date'), + 'staff::getName' => __('Agent Assigned'), + 'team::getName' => __('Team Assigned'), + 'thread_count' => __('Thread Count'), + 'attachment_count' => __('Attachment Count'), + ) + $cdata, + $how, + array('modify' => function(&$record, $keys) use ($fields) { + foreach ($fields as $k=>$f) { + if (($i = array_search($k, $keys)) !== false) { + $record[$i] = $f->export($f->to_php($record[$i])); + } + } + return $record; + }) + ); + } + + static function saveTasks($sql, $filename, $how='csv') { + + ob_start(); + self::dumpTasks($sql, $how); + $stuff = ob_get_contents(); + ob_end_clean(); + if ($stuff) + Http::download($filename, "text/$how", $stuff); + return false; } diff --git a/include/class.forms.php b/include/class.forms.php index dbd05a2b94e152849f15ee88d43f1e0943b98e97..ebc3c5588e6d58e24c55e60ad099fc965c4d0a54 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -22,6 +22,7 @@ class Form { static $renderer = 'GridFluidLayout'; static $id = 0; + var $options = array(); var $fields = array(); var $title = ''; var $instructions = ''; @@ -33,6 +34,7 @@ class Form { function __construct($source=null, $options=array()) { + $this->options = $options; if (isset($options['title'])) $this->title = $options['title']; if (isset($options['instructions'])) @@ -174,13 +176,13 @@ class Form { echo $this->getMedia(); } - function getLayout() { + function getLayout($title=false, $options=array()) { $rc = @$options['renderer'] ?: static::$renderer; return new $rc($title, $options); } - function asTable($options=array()) { - return $this->getLayout()->asTable($this); + function asTable($title=false, $options=array()) { + return $this->getLayout($title, $options)->asTable($this); // XXX: Media can't go in a table echo $this->getMedia(); } @@ -321,6 +323,26 @@ class SimpleForm extends Form { } } +class CustomForm extends SimpleForm { + + function getFields() { + global $thisstaff, $thisclient; + + $options = $this->options; + $user = $options['user'] ?: $thisstaff ?: $thisclient; + $isedit = ($options['mode'] == 'edit'); + $fields = array(); + foreach (parent::getFields() as $field) { + if ($isedit && !$field->isEditable($user)) + continue; + + $fields[] = $field; + } + + return $fields; + } +} + abstract class AbstractForm extends Form { function __construct($source=null, $options=array()) { parent::__construct($source, $options); @@ -347,6 +369,14 @@ interface FormRenderer { abstract class FormLayout { static $default_cell_layout = 'Cell'; + var $title; + var $options; + + function __construct($title=false, $options=array()) { + $this->title = $title; + $this->options = $options; + } + function getLayout($field) { $layout = $field->get('layout') ?: static::$default_cell_layout; if (is_string($layout)) @@ -362,13 +392,17 @@ implements FormRenderer { ob_start(); ?> <table class="<?php echo 'grid form' ?>"> - <caption><?php echo Format::htmlchars($form->getTitle()); ?> + <caption><?php echo Format::htmlchars($this->title ?: $form->getTitle()); ?> <div><small><?php echo Format::viewableImages($form->getInstructions()); ?></small></div> </caption> <tbody><tr><?php for ($i=0; $i<12; $i++) echo '<td style="width:8.3333%"/>'; ?></tr></tbody> <?php $row_size = 12; $cols = $row = 0; + + //Layout and rendering options + $options = $this->options; + foreach ($form->getFields() as $f) { $layout = $this->getLayout($f); $size = $layout->getWidth() ?: 12; @@ -386,7 +420,7 @@ implements FormRenderer { $attrs = array('colspan' => $size, 'rowspan' => $layout->getHeight(), 'style' => '"'.$layout->getOption('style').'"'); if ($offs) { ?> - <td colspan="<?php echo $offset; ?>"></td> <?php + <td colspan="<?php echo $offs; ?>"></td> <?php } ?> <td class="cell" <?php echo Format::array_implode('=', ' ', array_filter($attrs)); ?> @@ -398,6 +432,10 @@ implements FormRenderer { <label class="<?php if ($f->isRequired()) echo 'required'; ?>" for="<?php echo $f->getWidget()->id; ?>"> <?php echo Format::htmlchars($label); ?>: + <?php if ($f->isRequired()) { ?> + <span class="error">*</span> + <?php + }?> </label> <?php } if ($f->get('hint')) { ?> @@ -405,7 +443,7 @@ implements FormRenderer { <?php echo Format::htmlchars($f->get('hint')); ?> </div> <?php } - $f->render(); + $f->render($options); if ($f->errors()) foreach ($f->errors() as $e) echo sprintf('<div class="error">%s</div>', Format::htmlchars($e)); @@ -656,14 +694,21 @@ class FormField { } /** - * FIXME: Temp + * Check if the user has edit rights * */ - function isEditable() { - return (($this->get('flags') & DynamicFormField::FLAG_MASK_EDIT) == 0); + function isEditable($user=null) { + + if ($user instanceof Staff) + $flag = DynamicFormField::FLAG_AGENT_EDIT; + else + $flag = DynamicFormField::FLAG_CLIENT_EDIT; + + return (($this->get('flags') & $flag) != 0); } + /** * isStorable * @@ -1969,6 +2014,14 @@ class ThreadEntryField extends FormField { $config = $this->getConfiguration(); return $config['attachments']; } + + function getWidget($widgetClass=false) { + if ($hint = $this->get('hint')) + $this->set('placeholder', $hint); + $this->set('hint', null); + $widget = parent::getWidget($widgetClass); + return $widget; + } } class PriorityField extends ChoiceField { @@ -3347,7 +3400,7 @@ class DatetimePickerWidget extends Widget { } ?> <input type="text" name="<?php echo $this->name; ?>" - id="<?php echo $this->id; ?>" + id="<?php echo $this->id; ?>" style="display:inline-block;width:auto" value="<?php echo Format::htmlchars($this->value); ?>" size="12" autocomplete="off" class="dp" /> <script type="text/javascript"> @@ -3374,6 +3427,8 @@ class DatetimePickerWidget extends Widget { // TODO: Add time picker -- requires time picker or selection with // Misc::timeDropdown echo ' ' . Misc::timeDropdown($hr, $min, $this->name . ':time'); + + echo '</div>'; } /** @@ -3429,11 +3484,8 @@ class ThreadEntryWidget extends Widget { list($draft, $attrs) = Draft::getDraftAndDataAttrs($namespace, $object_id, $this->value); ?> - <span class="required"><?php - echo Format::htmlchars($this->field->getLocal('label')); - ?>: <span class="error">*</span></span><br/> <textarea style="width:100%;" name="<?php echo $this->field->get('name'); ?>" - placeholder="<?php echo Format::htmlchars($this->field->get('hint')); ?>" + placeholder="<?php echo Format::htmlchars($this->field->get('placeholder')); ?>" class="<?php if ($cfg->isRichTextEnabled()) echo 'richtext'; ?> draft draft-delete" <?php echo $attrs; ?> cols="21" rows="8" style="width:80%;"><?php echo diff --git a/include/class.misc.php b/include/class.misc.php index 4a7301782600c3cfe69b8b493d3ee02794bda8a8..69063c44229556fa6e1467224bfb4e01dc9d0604 100644 --- a/include/class.misc.php +++ b/include/class.misc.php @@ -160,7 +160,7 @@ class Misc { $min=0; ob_start(); - echo sprintf('<select name="%s" id="%s">',$name,$name); + echo sprintf('<select name="%s" id="%s" style="display:inline-block;width:auto">',$name,$name); echo '<option value="" selected>'.__('Time').'</option>'; for($i=23; $i>=0; $i--) { for($minute=45; $minute>=0; $minute-=15) { diff --git a/include/class.nav.php b/include/class.nav.php index e2dde777539cf2aa5157a68aa6fe4f559f01ae89..aafb4dc2562c92c4cc8cf09e2308cabf6343040a 100644 --- a/include/class.nav.php +++ b/include/class.nav.php @@ -177,7 +177,7 @@ class StaffNav { if($staff) { if ($staff->hasPerm(FAQ::PERM_MANAGE)) $subnav[]=array('desc'=>__('Categories'),'href'=>'categories.php','iconclass'=>'faq-categories'); - if ($cfg->isCannedResponseEnabled() && $staff->getRole()->hasPerm(Canned::PERM_MANAGE, false)) + if ($cfg->isCannedResponseEnabled() && $staff->hasPerm(Canned::PERM_MANAGE, false)) $subnav[]=array('desc'=>__('Canned Responses'),'href'=>'canned.php','iconclass'=>'canned'); } break; diff --git a/include/class.staff.php b/include/class.staff.php index 153bdc0a0d23a6b1d19b9379da7e33d36d9ef88c..6725a9c85ba105c4f619ac72f86af9e39ad6ee48 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -1047,7 +1047,7 @@ implements AuthenticatedUser, EmailContact, TemplateVariable { return !$errors; } - function updatePerms($vars, &$errors) { + function updatePerms($vars, &$errors=array()) { if (!$vars) { $this->permissions = ''; return; diff --git a/include/class.task.php b/include/class.task.php index 5f35fd5420d92575dcfa2b6afcc8a8718e79632e..5f9206e8069ced89007d7cace68f54d35ec2d4fb 100644 --- a/include/class.task.php +++ b/include/class.task.php @@ -63,6 +63,7 @@ class TaskModel extends VerySimpleModel { const PERM_EDIT = 'task.edit'; const PERM_ASSIGN = 'task.assign'; const PERM_TRANSFER = 'task.transfer'; + const PERM_REPLY = 'task.reply'; const PERM_CLOSE = 'task.close'; const PERM_DELETE = 'task.delete'; @@ -87,6 +88,11 @@ class TaskModel extends VerySimpleModel { /* @trans */ 'Transfer', 'desc' => /* @trans */ 'Ability to transfer tasks between departments'), + self::PERM_REPLY => array( + 'title' => + /* @trans */ 'Post Reply', + 'desc' => + /* @trans */ 'Ability to post task update'), self::PERM_CLOSE => array( 'title' => /* @trans */ 'Close', @@ -168,11 +174,11 @@ class TaskModel extends VerySimpleModel { return !$this->isOpen(); } - function close() { + protected function close() { return $this->clearFlag(self::ISOPEN); } - function reopen() { + protected function reopen() { return $this->setFlag(self::ISOPEN); } @@ -193,12 +199,36 @@ class TaskModel extends VerySimpleModel { RolePermission::register(/* @trans */ 'Tasks', TaskModel::getPermissions()); -class Task extends TaskModel implements Threadable { +class Task extends TaskModel implements RestrictedAccess, Threadable { var $form; var $entry; var $_thread; var $_entries; + var $_answers; + + var $lastrespondent; + + function __onload() { + $this->loadDynamicData(); + } + + function loadDynamicData() { + if (!isset($this->_answers)) { + $this->_answers = array(); + foreach (DynamicFormEntryAnswer::objects() + ->filter(array( + 'entry__object_id' => $this->getId(), + 'entry__object_type' => ObjectModel::OBJECT_TYPE_TASK + )) as $answer + ) { + $tag = mb_strtolower($answer->field->name) + ?: 'field.' . $answer->field->id; + $this->_answers[$tag] = $answer; + } + } + return $this->_answers; + } function getStatus() { return $this->isOpen() ? __('Open') : __('Completed'); @@ -281,6 +311,27 @@ class Task extends TaskModel implements Threadable { return $assignees ? implode($glue, $assignees):''; } + function getLastRespondent() { + + if (!isset($this->lastrespondent)) { + $this->lastrespondent = Staff::objects() + ->filter(array( + 'staff_id' => static::objects() + ->filter(array( + 'thread__entries__type' => 'R', + 'thread__entries__staff_id__gt' => 0 + )) + ->values_flat('thread__entries__staff_id') + ->order_by('-thread__entries__id') + ->limit(1) + )) + ->first() + ?: false; + } + + return $this->lastrespondent; + } + function getParticipants() { $participants = array(); foreach ($this->getThread()->collaborators as $c) @@ -386,23 +437,39 @@ class Task extends TaskModel implements Threadable { function setStatus($status, $comments='') { global $thisstaff; + $ecb = null; switch($status) { case 'open': if ($this->isOpen()) return false; $this->reopen(); + $this->closed = null; + + $ecb = function ($t) { + $t->logEvent('reopened', false, null, 'closed'); + }; break; case 'closed': if ($this->isClosed()) return false; + $this->close(); + $this->closed = SqlFunction::NOW(); + $ecb = function($t) { + $t->logEvent('closed'); + }; break; default: return false; } - $this->save(true); + if (!$this->save(true)) + return false; + + // Log events via callback + if ($ecb) $ecb($this); + if ($comments) { $errors = array(); $this->postNote(array( @@ -449,8 +516,15 @@ class Task extends TaskModel implements Threadable { } /* util routines */ + + function logEvent($state, $data=null, $user=null, $annul=null) { + $this->getThread()->getEvents()->log($this, $state, $data, $user, $annul); + } + function assign(AssignmentForm $form, &$errors, $alert=true) { + global $thisstaff; + $evd = array(); $assignee = $form->getAssignee(); if ($assignee instanceof Staff) { if ($this->getStaffId() == $assignee->getId()) { @@ -462,6 +536,10 @@ class Task extends TaskModel implements Threadable { $errors['assignee'] = __('Agent is unavailable for assignment'); } else { $this->staff_id = $assignee->getId(); + if ($thisstaff && $thisstaff->getId() == $assignee->getId()) + $evd['claim'] = true; + else + $evd['staff'] = array($assignee->getId(), $assignee->getName()); } } elseif ($assignee instanceof Team) { if ($this->getTeamId() == $assignee->getId()) { @@ -471,7 +549,7 @@ class Task extends TaskModel implements Threadable { ); } else { $this->team_id = $assignee->getId(); - + $evd = array('team' => $assignee->getId()); } } else { $errors['assignee'] = __('Unknown assignee'); @@ -480,6 +558,8 @@ class Task extends TaskModel implements Threadable { if ($errors || !$this->save(true)) return false; + $this->logEvent('assigned', $evd); + $this->onAssignment($assignee, $form->getField('comments')->getClean(), $alert); @@ -487,39 +567,90 @@ class Task extends TaskModel implements Threadable { return true; } - function onAssignment($assignee, $note='', $alert=true) { - global $thisstaff; + function onAssignment($assignee, $comments='', $alert=true) { + global $thisstaff, $cfg; if (!is_object($assignee)) return false; $assigner = $thisstaff ?: __('SYSTEM (Auto Assignment)'); + //Assignment completed... post internal note. - $title = sprintf(__('Task assigned to %s'), - (string) $assignee); + $note = null; + if ($comments) { + + $title = sprintf(__('Task assigned to %s'), + (string) $assignee); - if (!$note) { - $note = $title; - $title = ''; + $errors = array(); + $note = $this->postNote( + array('note' => $comments, 'title' => $title), + $errors, + $assigner, + false); } - $errors = array(); - $note = $this->postNote( - array('note' => $note, 'title' => $title), - $errors, - $assigner, - false); - - // Send alerts out - if (!$alert) + // Send alerts out if enabled. + if (!$alert || !$cfg->alertONTaskAssignment()) return false; + if (!($dept=$this->getDept()) + || !($tpl = $dept->getTemplate()) + || !($email = $dept->getAlertEmail()) + ) { + return true; + } + + // Recipients + $recipients = array(); + if ($assignee instanceof Staff) { + if ($cfg->alertStaffONTaskAssignment()) + $recipients[] = $assignee; + } elseif (($assignee instanceof Team) && $assignee->alertsEnabled()) { + if ($cfg->alertTeamMembersONTaskAssignment() && ($members=$assignee->getMembers())) + $recipients = array_merge($recipients, $members); + elseif ($cfg->alertTeamLeadONTaskAssignment() && ($lead=$assignee->getTeamLead())) + $recipients[] = $lead; + } + + if ($recipients + && ($msg=$tpl->getTaskAssignmentAlertMsgTemplate())) { + + $msg = $this->replaceVars($msg->asArray(), + array('comments' => $comments, + 'assignee' => $assignee, + 'assigner' => $assigner + ) + ); + // Send the alerts. + $sentlist = array(); + $options = $note instanceof ThreadEntry + ? array( + 'inreplyto' => $note->getEmailMessageId(), + 'references' => $note->getEmailReferences(), + 'thread' => $note) + : array(); + + foreach ($recipients as $k => $staff) { + if (!is_object($staff) + || !$staff->isAvailable() + || in_array($staff->getEmail(), $sentlist)) { + continue; + } + + $alert = $this->replaceVars($msg, array('recipient' => $staff)); + $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options); + $sentlist[] = $staff->getEmail(); + } + } + return true; } function transfer(TransferForm $form, &$errors, $alert=true) { - global $thisstaff; + global $thisstaff, $cfg; + $cdept = $this->getDept(); $dept = $form->getDept(); if (!$dept || !($dept instanceof Dept)) $errors['dept'] = __('Department selection required'); @@ -531,25 +662,77 @@ class Task extends TaskModel implements Threadable { if ($errors || !$this->save()) return false; - // Transfer completed... post internal note. - $title = sprintf(__('%s transferred to %s department'), - __('Task'), - $dept->getName()); + // Log transfer event + $this->logEvent('transferred'); + // Post internal note if any $note = $form->getField('comments')->getClean(); - if (!$note) { - $note = $title; - $title = ''; + if ($note) { + $title = sprintf(__('%1$s transferred from %2$s to %3$s'), + __('Task'), + $cdept->getName(), + $dept->getName()); + + $_errors = array(); + $note = $this->postNote( + array('note' => $note, 'title' => $title), + $_errors, $thisstaff, false); } - $_errors = array(); - $note = $this->postNote( - array('note' => $note, 'title' => $title), - $_errors, $thisstaff, false); // Send alerts if requested && enabled. - if (!$alert) + if (!$alert || !$cfg->alertONTaskTransfer()) return true; + if (($email = $dept->getAlertEmail()) + && ($tpl = $dept->getTemplate()) + && ($msg=$tpl->getTaskTransferAlertMsgTemplate())) { + + $msg = $this->replaceVars($msg->asArray(), + array('comments' => $note, 'staff' => $thisstaff)); + // Recipients + $recipients = array(); + // Assigned staff or team... if any + if ($this->isAssigned() && $cfg->alertAssignedONTaskTransfer()) { + if($this->getStaffId()) + $recipients[] = $this->getStaff(); + elseif ($this->getTeamId() + && ($team=$this->getTeam()) + && ($members=$team->getMembers()) + ) { + $recipients = array_merge($recipients, $members); + } + } elseif ($cfg->alertDeptMembersONTaskTransfer() && !$this->isAssigned()) { + // Only alerts dept members if the task is NOT assigned. + if ($members = $dept->getMembersForAlerts()) + $recipients = array_merge($recipients, $members); + } + + // Always alert dept manager?? + if ($cfg->alertDeptManagerONTaskTransfer() + && ($manager=$dept->getManager())) { + $recipients[] = $manager; + } + + $sentlist = $options = array(); + if ($note instanceof ThreadEntry) { + $options += array( + 'inreplyto'=>$note->getEmailMessageId(), + 'references'=>$note->getEmailReferences(), + 'thread'=>$note); + } + + foreach ($recipients as $k=>$staff) { + if (!is_object($staff) + || !$staff->isAvailable() + || in_array($staff->getEmail(), $sentlist) + ) { + continue; + } + $alert = $this->replaceVars($msg, array('recipient' => $staff)); + $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options); + $sentlist[] = $staff->getEmail(); + } + } return true; } @@ -569,12 +752,78 @@ class Task extends TaskModel implements Threadable { if (!($note=$this->getThread()->addNote($vars, $errors))) return null; + $assignee = $this->getStaff(); + if (isset($vars['task_status'])) $this->setStatus($vars['task_status']); + $this->onActivity(array( + 'activity' => $note->getActivity(), + 'threadentry' => $note, + 'assignee' => $assignee + ), $alert); + return $note; } + /* public */ + function postReply($vars, &$errors, $alert = true) { + global $thisstaff, $cfg; + + + if (!$vars['poster'] && $thisstaff) + $vars['poster'] = $thisstaff; + + if (!$vars['staffId'] && $thisstaff) + $vars['staffId'] = $thisstaff->getId(); + + if (!$vars['ip_address'] && $_SERVER['REMOTE_ADDR']) + $vars['ip_address'] = $_SERVER['REMOTE_ADDR']; + + if (!($response = $this->getThread()->addResponse($vars, $errors))) + return null; + + $assignee = $this->getStaff(); + // Set status - if checked. + if ($vars['reply_status_id'] + && $vars['reply_status_id'] != $this->getStatusId() + ) { + $this->setStatus($vars['reply_status_id']); + } + + /* + // TODO: add auto claim setting for tasks. + // Claim on response bypasses the department assignment restrictions + if ($thisstaff + && $this->isOpen() + && !$this->getStaffId() + && $cfg->autoClaimTasks) + ) { + $this->staff_id = $thisstaff->getId(); + } + */ + + $this->lastrespondent = $response->staff; + $this->save(); + + // Send activity alert to agents + $activity = $vars['activity'] ?: $response->getActivity(); + $this->onActivity( array( + 'activity' => $activity, + 'threadentry' => $response, + 'assignee' => $assignee, + )); + // Send alert to collaborators + if ($alert && $vars['emailcollab']) { + $signature = ''; + $this->notifyCollaborators($response, + array('signature' => $signature) + ); + } + + return $response; + } + function pdfExport($options=array()) { global $thisstaff; @@ -596,12 +845,283 @@ class Task extends TaskModel implements Threadable { exit; } + /* util routines */ + function replaceVars($input, $vars = array()) { + global $ost; + + return $ost->replaceTemplateVariables($input, + array_merge($vars, array('task' => $this))); + } + + function asVar() { + return $this->getNumber(); + } + + function getVar($tag) { + global $cfg; + + if ($tag && is_callable(array($this, 'get'.ucfirst($tag)))) + return call_user_func(array($this, 'get'.ucfirst($tag))); + + switch(mb_strtolower($tag)) { + case 'phone': + case 'phone_number': + return $this->getPhoneNumber(); + break; + case 'staff_link': + return sprintf('%s/scp/tasks.php?id=%d', $cfg->getBaseUrl(), $this->getId()); + break; + case 'create_date': + return new FormattedDate($this->getCreateDate()); + break; + case 'due_date': + if ($due = $this->getEstDueDate()) + return new FormattedDate($due); + break; + case 'close_date': + if ($this->isClosed()) + return new FormattedDate($this->getCloseDate()); + break; + case 'last_update': + return new FormattedDate($this->last_update); + default: + if (isset($this->_answers[$tag])) + // The answer object is retrieved here which will + // automatically invoke the toString() method when the + // answer is coerced into text + return $this->_answers[$tag]; + } + return false; + } + + static function getVarScope() { + $base = array( + 'assigned' => __('Assigned agent and/or team'), + 'close_date' => array( + 'class' => 'FormattedDate', 'desc' => __('Date Closed'), + ), + 'create_date' => array( + 'class' => 'FormattedDate', 'desc' => __('Date created'), + ), + 'dept' => array( + 'class' => 'Dept', 'desc' => __('Department'), + ), + 'due_date' => array( + 'class' => 'FormattedDate', 'desc' => __('Due Date'), + ), + 'number' => __('Task number'), + 'recipients' => array( + 'class' => 'UserList', 'desc' => __('List of all recipient names'), + ), + 'status' => __('Status'), + 'staff' => array( + 'class' => 'Staff', 'desc' => __('Assigned/closing agent'), + ), + 'subject' => 'Subject', + 'team' => array( + 'class' => 'Team', 'desc' => __('Assigned/closing team'), + ), + 'thread' => array( + 'class' => 'TaskThread', 'desc' => __('Task Thread'), + ), + 'last_update' => array( + 'class' => 'FormattedDate', 'desc' => __('Time of last update'), + ), + ); + + $extra = VariableReplacer::compileFormScope(TaskForm::getInstance()); + return $base + $extra; + } + + function onActivity($vars, $alert=true) { + global $cfg, $thisstaff; + + if (!$alert // Check if alert is enabled + || !$cfg->alertONTaskActivity() + || !($dept=$this->getDept()) + || !($email=$cfg->getAlertEmail()) + || !($tpl = $dept->getTemplate()) + || !($msg=$tpl->getTaskActivityAlertMsgTemplate()) + ) { + return; + } + + // Alert recipients + $recipients = array(); + //Last respondent. + if ($cfg->alertLastRespondentONTaskActivity()) + $recipients[] = $this->getLastRespondent(); + + // Assigned staff / team + if ($cfg->alertAssignedONTaskActivity()) { + if (isset($vars['assignee']) + && $vars['assignee'] instanceof Staff) + $recipients[] = $vars['assignee']; + elseif ($this->isOpen() && ($assignee = $this->getStaff())) + $recipients[] = $assignee; + + if ($team = $this->getTeam()) + $recipients = array_merge($recipients, $team->getMembers()); + } + + // Dept manager + if ($cfg->alertDeptManagerONTaskActivity() && $dept && $dept->getManagerId()) + $recipients[] = $dept->getManager(); + + $options = array(); + $staffId = $thisstaff ? $thisstaff->getId() : 0; + if ($vars['threadentry'] && $vars['threadentry'] instanceof ThreadEntry) { + $options = array( + 'inreplyto' => $vars['threadentry']->getEmailMessageId(), + 'references' => $vars['threadentry']->getEmailReferences(), + 'thread' => $vars['threadentry']); + + // Activity details + if (!$vars['message']) + $vars['message'] = $vars['threadentry']; + + // Staff doing the activity + $staffId = $vars['threadentry']->getStaffId() ?: $staffId; + } + + $msg = $this->replaceVars($msg->asArray(), + array( + 'note' => $vars['threadentry'], // For compatibility + 'activity' => $vars['activity'], + 'message' => $vars['message'])); + + $isClosed = $this->isClosed(); + $sentlist=array(); + foreach ($recipients as $k=>$staff) { + if (!is_object($staff) + // Don't bother vacationing staff. + || !$staff->isAvailable() + // No need to alert the poster! + || $staffId == $staff->getId() + // No duplicates. + || isset($sentlist[$staff->getEmail()]) + // Make sure staff has access to task + || ($isClosed && !$this->checkStaffPerm($staff)) + ) { + continue; + } + $alert = $this->replaceVars($msg, array('recipient' => $staff)); + $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options); + $sentlist[$staff->getEmail()] = 1; + } + + } + + /* + * Notify collaborators on response or new message + * + */ + function notifyCollaborators($entry, $vars = array()) { + global $cfg; + + if (!$entry instanceof ThreadEntry + || !($recipients=$this->getThread()->getParticipants()) + || !($dept=$this->getDept()) + || !($tpl=$dept->getTemplate()) + || !($msg=$tpl->getTaskActivityNoticeMsgTemplate()) + || !($email=$dept->getEmail()) + ) { + return; + } + + // Who posted the entry? + $skip = array(); + if ($entry instanceof Message) { + $poster = $entry->getUser(); + // Skip the person who sent in the message + $skip[$entry->getUserId()] = 1; + // Skip all the other recipients of the message + foreach ($entry->getAllEmailRecipients() as $R) { + foreach ($recipients as $R2) { + if (0 === strcasecmp($R2->getEmail(), $R->mailbox.'@'.$R->host)) { + $skip[$R2->getUserId()] = true; + break; + } + } + } + } else { + $poster = $entry->getStaff(); + } + + $vars = array_merge($vars, array( + 'message' => (string) $entry, + 'poster' => $poster ?: _S('A collaborator'), + ) + ); + + $msg = $this->replaceVars($msg->asArray(), $vars); + + $attachments = $cfg->emailAttachments()?$entry->getAttachments():array(); + $options = array('inreplyto' => $entry->getEmailMessageId(), + 'thread' => $entry); + + foreach ($recipients as $recipient) { + // Skip folks who have already been included on this part of + // the conversation + if (isset($skip[$recipient->getUserId()])) + continue; + $notice = $this->replaceVars($msg, array('recipient' => $recipient)); + $email->send($recipient, $notice['subj'], $notice['body'], $attachments, + $options); + } + } + + function update($forms, $vars, &$errors) { + global $thisstaff; + + + if (!$forms || !$this->checkStaffPerm($thisstaff, Task::PERM_EDIT)) + return false; + + + foreach ($forms as $form) { + $form->setSource($vars); + if (!$form->isValid(function($f) { + return $f->isVisibleToStaff() && $f->isEditableToStaff(); + }, array('mode'=>'edit'))) { + $errors = array_merge($errors, $form->errors()); + } + } + + if ($errors) + return false; + + // Update dynamic meta-data + $changes = array(); + foreach ($forms as $f) { + $changes += $f->getChanges(); + $f->save(); + } + + + if ($vars['note']) { + $_errors = array(); + $this->postNote(array( + 'note' => $vars['note'], + 'title' => __('Task Update'), + ), + $_errors, + $thisstaff); + } + + if ($changes) + $this->logEvent('edited', array('fields' => $changes)); + + Signal::send('model.updated', $this); + return $this->save(); + } + + /* static routines */ static function lookupIdByNumber($number) { - $sql = 'SELECT id FROM '.TASK_TABLE - .' WHERE `number`='.db_input($number); - list($id) = db_fetch_row(db_query($sql)); - return $id; + if (($task = self::lookup(array('number' => $number)))) + return $task->getId(); + } static function isNumberUnique($number) { @@ -710,7 +1230,7 @@ class Task extends TaskModel implements Threadable { .sprintf('task.flags & %d != 0 ', TaskModel::ISOPEN) .')'; - if(!$staff->showAssignedOnly() && ($depts=$staff->getDepts())) //Staff with limited access just see Assigned tickets. + if(!$staff->showAssignedOnly() && ($depts=$staff->getDepts())) //Staff with limited access just see Assigned tasks. $where[] = 'task.dept_id IN('.implode(',', db_input($depts)).') '; $where = implode(' OR ', $where); @@ -807,31 +1327,36 @@ class TaskForm extends DynamicForm { return static::$instance; } - static function getInternalForm($source=null) { + static function getInternalForm($source=null, $options=array()) { if (!isset(static::$internalForm)) - static::$internalForm = new SimpleForm(self::getInternalFields(), $source); + static::$internalForm = new TaskInternalForm($source, $options); return static::$internalForm; } +} - static function getInternalFields() { - return array( +class TaskInternalForm +extends AbstractForm { + static $layout = 'GridFormLayout'; + + function buildFields() { + + $fields = array( 'dept_id' => new DepartmentField(array( 'id'=>1, 'label' => __('Department'), - 'flags' => hexdec(0X450F3), 'required' => true, + 'layout' => new GridFluidCell(6), )), 'staff_id' => new AssigneeField(array( 'id'=>2, 'label' => __('Assignee'), - 'flags' => hexdec(0X450F3), 'required' => false, + 'layout' => new GridFluidCell(6), )), 'duedate' => new DatetimeField(array( 'id' => 3, 'label' => __('Due Date'), - 'flags' => hexdec(0X450B3), 'required' => false, 'configuration' => array( 'min' => Misc::gmtime(), @@ -842,6 +1367,14 @@ class TaskForm extends DynamicForm { )), ); + + $mode = @$this->options['mode']; + if ($mode && $mode == 'edit') { + unset($fields['dept_id']); + unset($fields['staff_id']); + } + + return $fields; } } diff --git a/include/class.template.php b/include/class.template.php index f3a459f6de0855b27c0aca7647107d2733e8481e..bc998c8516240abb0f0b8dc24ee21433e96f675a 100644 --- a/include/class.template.php +++ b/include/class.template.php @@ -136,32 +136,49 @@ class EmailTemplateGroup { 'group'=>'c.task', 'name'=>/* @trans */ 'New Task Alert', 'desc'=>/* @trans */ 'Alert sent to agents, if enabled, on new task.', + 'context' => array( + 'task', 'recipient', 'message', + ), ), 'task.activity.notice' => array( 'group'=>'c.task', 'name'=>/* @trans */ 'New Activity Notice', - 'desc'=>/* @trans */ 'Template used to notify collaborators on task activity.' + 'desc'=>/* @trans */ 'Template used to notify collaborators on task activity.', + 'context' => array( + 'task', 'signature', 'message', 'poster', 'recipient', + ), ), 'task.activity.alert'=>array( 'group'=>'c.task', 'name'=>/* @trans */ 'New Activity Alert', 'desc'=>/* @trans */ 'Alert sent to selected agents, if enabled, on new activity.', - + 'context' => array( + 'task', 'recipient', 'note', 'comments', 'activity', + ), ), 'task.assignment.alert' => array( 'group'=>'c.task', 'name'=>/* @trans */ 'Task Assignment Alert', 'desc'=>/* @trans */ 'Alert sent to agents on task assignment.', + 'context' => array( + 'task', 'recipient', 'comments', 'assignee', 'assigner', + ), ), 'task.transfer.alert'=>array( 'group'=>'c.task', 'name'=>/* @trans */ 'Task Transfer Alert', 'desc'=>/* @trans */ 'Alert sent to agents on task transfer.', + 'context' => array( + 'task', 'recipient', 'note', 'comments', 'activity', + ), ), 'task.overdue.alert'=>array( 'group'=>'c.task', 'name'=>/* @trans */ 'Overdue Task Alert', 'desc'=>/* @trans */ 'Alert sent to agents on stale or overdue task.', + 'context' => array( + 'task', 'recipient', 'comments', + ), ), ); @@ -337,6 +354,10 @@ class EmailTemplateGroup { return $this->getMsgTemplate('task.alert'); } + function getTaskActivityAlertMsgTemplate() { + return $this->getMsgTemplate('task.activity.alert'); + } + function getTaskActivityNoticeMsgTemplate() { return $this->getMsgTemplate('task.activity.notice'); } diff --git a/include/class.thread.php b/include/class.thread.php index 9df67a1e0142bd15b2d15c6e96afb95227cc2edd..c0fbb8fec54c199573d176ddac58532e57ff3ea4 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -54,6 +54,7 @@ class Thread extends VerySimpleModel { var $_object; var $_collaborators; // Cache for collabs + var $_participants; function getId() { return $this->id; @@ -211,13 +212,33 @@ class Thread extends VerySimpleModel { return true; } + + + //UserList of participants (collaborators) + function getParticipants() { + + if (!isset($this->_participants)) { + $list = new UserList(); + if ($collabs = $this->getActiveCollaborators()) { + foreach ($collabs as $c) + $list->add($c); + } + + $this->_participants = $list; + } + + return $this->_participants; + } + + // Render thread function render($type=false, $options=array()) { $mode = $options['mode'] ?: self::MODE_STAFF; // Register thread actions prior to rendering the thread. - include_once INCLUDE_DIR . 'class.thread_actions.php'; + if (!class_exists('tea_showemailheaders')) + include_once INCLUDE_DIR . 'class.thread_actions.php'; $entries = $this->getEntries(); if ($type && is_array($type)) @@ -1037,6 +1058,10 @@ implements TemplateVariable { return $this->email_info->save(); } + function getActivity() { + return new ThreadActivity('', ''); + } + /* variables */ function __toString() { @@ -2234,6 +2259,12 @@ class ResponseThreadEntry extends ThreadEntry { const ENTRY_TYPE = 'R'; + function getActivity() { + return new ThreadActivity( + _S('New Response'), + _S('New response posted')); + } + function getSubject() { return $this->getTitle(); } @@ -2283,6 +2314,12 @@ class NoteThreadEntry extends ThreadEntry { return $this->getBody(); } + function getActivity() { + return new ThreadActivity( + _S('New Internal Note'), + _S('New internal note posted')); + } + static function create($vars, &$errors) { return self::add($vars, $errors); } @@ -2566,4 +2603,46 @@ interface Threadable { function getThread(); function postThreadEntry($type, $vars, $options=array()); } + +/** + * ThreadActivity + * + * Object to thread activity + * + */ +class ThreadActivity implements TemplateVariable { + var $title; + var $desc; + + function __construct($title, $desc) { + $this->title = $title; + $this->desc = $desc; + } + + function getTitle() { + return $this->title; + } + + function getDescription() { + return $this->desc; + } + function asVar() { + return (string) $this->getTitle(); + } + + function getVar($tag) { + if ($tag && is_callable(array($this, 'get'.ucfirst($tag)))) + return call_user_func(array($this, 'get'.ucfirst($tag))); + + return false; + } + + static function getVarScope() { + return array( + 'title' => __('Activity Title'), + 'description' => __('Activity Description'), + ); + } +} + ?> diff --git a/include/class.thread_actions.php b/include/class.thread_actions.php index df4cf37e02e1d48cb087a47b293053d20ab17236..428793399807d245f86cf0592041606784cff557 100644 --- a/include/class.thread_actions.php +++ b/include/class.thread_actions.php @@ -277,10 +277,11 @@ class TEA_EditAndResendThreadEntry extends TEA_EditThreadEntry { function resend($response) { global $cfg, $thisstaff; - $vars = $_POST; - $ticket = $response->getThread()->getObject(); + if (!($object = $response->getThread()->getObject())) + return false; - $dept = $ticket->getDept(); + $vars = $_POST; + $dept = $object->getDept(); $poster = $response->getStaff(); if ($thisstaff && $vars['signature'] == 'mine') @@ -299,23 +300,25 @@ class TEA_EditAndResendThreadEntry extends TEA_EditThreadEntry { 'poster' => $response->getStaff()); $options = array('thread' => $response); - if (($email=$dept->getEmail()) - && ($tpl = $dept->getTemplate()) - && ($msg=$tpl->getReplyMsgTemplate()) - ) { - $msg = $ticket->replaceVars($msg->asArray(), - $variables + array('recipient' => $ticket->getOwner())); + // Resend response to collabs + if (($object instanceof Ticket) + && ($email=$dept->getEmail()) + && ($tpl = $dept->getTemplate()) + && ($msg=$tpl->getReplyMsgTemplate())) { + + $msg = $object->replaceVars($msg->asArray(), + $variables + array('recipient' => $object->getOwner())); $attachments = $cfg->emailAttachments() ? $response->getAttachments() : array(); - $email->send($ticket->getOwner(), $msg['subj'], $msg['body'], + $email->send($object->getOwner(), $msg['subj'], $msg['body'], $attachments, $options); } // TODO: Add an option to the dialog - $ticket->notifyCollaborators($response, array('signature' => $signature)); + $object->notifyCollaborators($response, array('signature' => $signature)); // Log an event that the item was resent - $ticket->logEvent('resent', array('entry' => $response->id)); + $object->logEvent('resent', array('entry' => $response->id)); // Flag the entry as resent $response->flags |= ThreadEntry::FLAG_RESENT; diff --git a/include/class.variable.php b/include/class.variable.php index 8c10fc61c76d245e9c7c0f2ad4ee1a6ca5ed184e..76acb1d0f2d2d789e312ee4554db8c40b0e4b630 100644 --- a/include/class.variable.php +++ b/include/class.variable.php @@ -262,7 +262,7 @@ class VariableReplacer { return false; $contextTypes = array( - 'activity' => __('Type of recent activity'), + 'activity' => array('class' => 'ThreadActivity', 'desc' => __('Type of recent activity')), 'assignee' => array('class' => 'Staff', 'desc' => __('Assigned agent/team')), 'assigner' => array('class' => 'Staff', 'desc' => __('Agent performing the assignment')), 'comments' => __('Assign/transfer comments'), @@ -276,6 +276,7 @@ class VariableReplacer { 'signature' => 'Selected staff or department signature', 'staff' => array('class' => 'Staff', 'desc' => 'Agent originating the activity'), 'ticket' => array('class' => 'Ticket', 'desc' => 'The ticket'), + 'task' => array('class' => 'Task', 'desc' => 'The task'), 'user' => array('class' => 'User', 'desc' => __('Message recipient')), ); $context = array(); diff --git a/include/i18n/en_US/form.yaml b/include/i18n/en_US/form.yaml index cea0b559ceba8378f1dfc571eebe780d750d5c2a..9a807a05e86b4d87461bed3a6947bc8d7dfecef2 100644 --- a/include/i18n/en_US/form.yaml +++ b/include/i18n/en_US/form.yaml @@ -191,7 +191,7 @@ fields: - type: text # notrans name: title # notrans - flags: 0x470B1 + flags: 0x470A1 sort: 1 label: Title configuration: @@ -199,7 +199,7 @@ length: 50 - type: thread # notrans name: description # notrans - flags: 0x450B3 + flags: 0x450F3 sort: 2 label: Description hint: Details on the reason(s) for creating the task. diff --git a/include/i18n/en_US/help/tips/tasks.queue.yaml b/include/i18n/en_US/help/tips/tasks.queue.yaml new file mode 100644 index 0000000000000000000000000000000000000000..393447f0b16260e6e1f2166d0453c94091794587 --- /dev/null +++ b/include/i18n/en_US/help/tips/tasks.queue.yaml @@ -0,0 +1,28 @@ +# +# This is popup help messages for the Agents Panel -> Tasks +# +# Fields: +# title - Shown in bold at the top of the popover window +# content - The body of the help popover +# links - List of links shows below the content +# title - Link title +# href - href of link (links starting with / are translated to the +# helpdesk installation path) +# +# The key names such as 'helpdesk_name' should not be translated as they +# must match the HTML #ids put into the page template. +# +--- +advanced: + title: Advanced + content: > + Narrow down your search parameters. Once you have selected your advanced + search criteria and run the search, you can <span class="doc-desc-title">Export + </span> the data at the bottom of the page. + +export: + title: Export + content: > + Export your data currently in view in a CSV file. + CSV files may be opened with any spreadsheet software + (i.e., Microsoft Excel, Apple Pages, OpenOffice, etc.). diff --git a/include/i18n/en_US/templates/email/task.activity.alert.yaml b/include/i18n/en_US/templates/email/task.activity.alert.yaml index 82e7c4ce04474cf4214844d8492560074451dc1d..5c94582edca104cdacf12fb1b2333933d7b09206 100644 --- a/include/i18n/en_US/templates/email/task.activity.alert.yaml +++ b/include/i18n/en_US/templates/email/task.activity.alert.yaml @@ -13,34 +13,12 @@ notes: | or visits the web portal and posts a new message there. subject: | - New Activity Alert + Task Activity [#%{task.number}] - %{activity.title} body: | <h3><strong>Hi %{recipient.name},</strong></h3> - New message appended to task <a - href="%{ticket.staff_link}">#%{task.number}</a> + Task <a href="%{task.staff_link}">#%{task.number}</a> updated: %{activity.description} <br> <br> - <table> - <tbody> - <tr> - <td> - <strong>Title</strong>: - </td> - <td> - %{task.title} - </td> - </tr> - <tr> - <td> - <strong>Department</strong>: - </td> - <td> - %{task.dept.name} - </td> - </tr> - </tbody> - </table> - <br> %{message} <br> <br> diff --git a/include/i18n/en_US/templates/email/task.activity.notice.yaml b/include/i18n/en_US/templates/email/task.activity.notice.yaml index 45d1e3e51aecdf64851b0f54fc230f71824be969..750e4a09cd5c892ad1ec5e756bd1fbf368651e7b 100644 --- a/include/i18n/en_US/templates/email/task.activity.notice.yaml +++ b/include/i18n/en_US/templates/email/task.activity.notice.yaml @@ -5,7 +5,7 @@ # --- notes: | - Notice sent to collaborators on task activity e.g note or message. + Notice sent to collaborators on task activity e.g reply or message. subject: | Re: %{task.title} [#%{task.number}] @@ -20,9 +20,6 @@ body: | <br> <hr> <div style="color: rgb(127, 127, 127); font-size: small; text-align: center;"> - <em>You're getting this email because you are a collaborator - on task <a href="%{recipient.task_link}" style="color: rgb(84, 141, 212);" - >#%{task.number}</a>. To participate, simply reply to this email - or <a href="%{recipient.task_link}" style="color: rgb(84, 141, 212);" - >click here</a> for a complete archive of the task thread.</em> + <em>You're getting this email because you are a collaborator on task + #%{task.number}. To participate, simply reply to this email.</em> </div> diff --git a/include/i18n/en_US/templates/email/task.alert.yaml b/include/i18n/en_US/templates/email/task.alert.yaml index 08e311fe09f322b8c5606fb1d4e28d8d614aa678..ee6760803c6bb1d052603f1feb9ce3675399ffdf 100644 --- a/include/i18n/en_US/templates/email/task.alert.yaml +++ b/include/i18n/en_US/templates/email/task.alert.yaml @@ -12,7 +12,7 @@ subject: | New Task Alert body: | <h2>Hi %{recipient.name},</h2> - New task #%{task.number} created + New task <a href="%{task.staff_link}">#%{task.number}</a> created <br> <br> <table> diff --git a/include/i18n/en_US/templates/email/task.assignment.alert.yaml b/include/i18n/en_US/templates/email/task.assignment.alert.yaml index 4c509b08bb812f629966688d727e09a494256ac9..2a8684c51d65ceac087757a2778285f1d215ab94 100644 --- a/include/i18n/en_US/templates/email/task.assignment.alert.yaml +++ b/include/i18n/en_US/templates/email/task.assignment.alert.yaml @@ -1,7 +1,7 @@ # # Email template: task.assignment.alert.yaml # -# Sent to agents when a ticket is assigned to them or the team to which +# Sent to agents when a task is assigned to them or the team to which # they belong. # Use %{assigner} to distinguish who made the assignment. # @@ -18,19 +18,6 @@ body: | assigned to you by %{assigner.name.short} <br> <br> - <table> - <tbody> - <tr> - <td> - <strong>Title</strong>: - </td> - <td> - %{task.title} - </td> - </tr> - </tbody> - </table> - <br> %{comments} <br> <br> diff --git a/include/i18n/en_US/templates/email/task.overdue.alert.yaml b/include/i18n/en_US/templates/email/task.overdue.alert.yaml index 0b48a7337e04efd6dbdf6c0f8c7cfeb15d7b463f..05a8573d3a52b88f9b8d153daba9c4ef313e779f 100644 --- a/include/i18n/en_US/templates/email/task.overdue.alert.yaml +++ b/include/i18n/en_US/templates/email/task.overdue.alert.yaml @@ -2,14 +2,12 @@ # Email template: task.overdue.alert.yaml # # Sent to agents when a tasks transitions to overdue in the system. -# Overdue tasks occur based on set due-date as well as the SLA -# defined for the task. +# Overdue tasks occur based on set due-date. # --- notes: | Sent to agents when a task transitions to overdue in the system. - Overdue tasks occur based on the set due-date as well as the SLA - defined for the task. + Overdue tasks occur based on the set due-date. subject: | Stale Task Alert diff --git a/include/staff/tasks.inc.php b/include/staff/tasks.inc.php index ab826da6aeb550dd0fdcb42c2a4c62a0d86f410b..5a3a80df938383678b5536c8918bcdd806223e35 100644 --- a/include/staff/tasks.inc.php +++ b/include/staff/tasks.inc.php @@ -1,5 +1,5 @@ <?php -$tasks = TaskModel::objects(); +$tasks = Task::objects(); $date_header = $date_col = false; // Figure out REFRESH url — which might not be accurate after posting a @@ -14,26 +14,41 @@ unset($args['a']); $refresh_url = $path . '?' . http_build_query($args); +$sort_options = array( + 'updated' => __('Most Recently Updated'), + 'created' => __('Most Recently Created'), + 'due' => __('Due Soon'), + 'number' => __('Task Number'), + 'closed' => __('Most Recently Closed'), + 'hot' => __('Longest Thread'), + 'relevance' => __('Relevance'), +); + $queue_name = strtolower($_GET['status'] ?: $_GET['a']); //Status is overloaded switch ($queue_name) { case 'closed': $status='closed'; $results_type=__('Closed Tasks'); $showassigned=true; //closed by. + $queue_sort_options = array('closed', 'updated', 'created', 'number', 'hot'); + break; case 'overdue': $status='open'; $results_type=__('Overdue Tasks'); $tasks->filter(array('isoverdue'=>1)); + $queue_sort_options = array('updated', 'created', 'number', 'hot'); break; case 'assigned': $status='open'; $staffId=$thisstaff->getId(); $results_type=__('My Tasks'); $tasks->filter(array('staff_id'=>$thisstaff->getId())); + $queue_sort_options = array('updated', 'created', 'hot', 'number'); break; default: case 'search': + $queue_sort_options = array('closed', 'updated', 'created', 'number', 'hot'); // Consider basic search if ($_REQUEST['query']) { $results_type=__('Search Results'); @@ -55,17 +70,22 @@ case 'search': case 'open': $status='open'; $results_type=__('Open Tasks'); + $queue_sort_options = array('created', 'updated', 'due', 'number', 'hot'); break; } // Apply filters $filters = array(); -$SQ = new Q(array('flags__hasbit' => TaskModel::ISOPEN)); -if ($status && !strcasecmp($status, 'closed')) - $SQ->negate(); +if ($status) { + $SQ = new Q(array('flags__hasbit' => TaskModel::ISOPEN)); + if (!strcasecmp($status, 'closed')) + $SQ->negate(); + + $filters[] = $SQ; +} -$filters[] = $SQ; -$tasks->filter($filters); +if ($filters) + $tasks->filter($filters); // Impose visibility constraints // ------------------------------------------------------------ @@ -106,29 +126,39 @@ $tasks->values('id', 'number', 'created', 'staff_id', 'team_id', 'dept__name', 'cdata__title', 'flags'); // Apply requested quick filter -// Apply requested sorting -$queue_sort_key = sprintf(':Q:%s:sort', $queue_name); +$queue_sort_key = sprintf(':Q%s:%s:sort', ObjectModel::OBJECT_TYPE_TASK, $queue_name); + +if (isset($_GET['sort'])) { + $_SESSION[$queue_sort_key] = $_GET['sort']; +} +elseif (!isset($_SESSION[$queue_sort_key])) { + $_SESSION[$queue_sort_key] = $queue_sort_options[0]; +} -if (isset($_GET['sort'])) - $_SESSION[$queue_sort_key] = $_GET['sort']; switch ($_SESSION[$queue_sort_key]) { case 'number': $tasks->extra(array( 'order_by'=>array(SqlExpression::times(new SqlField('number'), 1)) )); break; -case 'priority,due': - $tasks->order_by('cdata__:priority__priority_urgency'); - // Fall through to add in due date filter case 'due': $date_header = __('Due Date'); - $date_col = 'est_duedate'; - $tasks->values('est_duedate'); - $tasks->filter(array('est_duedate__isnull'=>false)); - $tasks->order_by('est_duedate'); + $date_col = 'duedate'; + $tasks->values('duedate'); + $tasks->filter(array('duedate__isnull'=>false)); + $tasks->order_by('duedate'); break; case 'updated': - $tasks->order_by('cdata__:priority__priority_urgency', '-lastupdate'); + $date_header = __('Last Updated'); + $date_col = 'updated'; + $tasks->values('updated'); + $tasks->order_by('-updated'); + break; +case 'hot': + $tasks->order_by('-thread_count'); + $tasks->annotate(array( + 'thread_count' => SqlAggregate::COUNT('thread__entries'), + )); break; case 'created': default: @@ -150,7 +180,7 @@ $_SESSION[':Q:tasks'] = $tasks; // Mass actions $actions = array(); -if ($thisstaff->hasPerm(Task::PERM_ASSIGN)) { +if ($thisstaff->hasPerm(Task::PERM_ASSIGN, false)) { $actions += array( 'assign' => array( 'icon' => 'icon-user', @@ -158,7 +188,7 @@ if ($thisstaff->hasPerm(Task::PERM_ASSIGN)) { )); } -if ($thisstaff->hasPerm(Task::PERM_TRANSFER)) { +if ($thisstaff->hasPerm(Task::PERM_TRANSFER, false)) { $actions += array( 'transfer' => array( 'icon' => 'icon-share', @@ -166,7 +196,7 @@ if ($thisstaff->hasPerm(Task::PERM_TRANSFER)) { )); } -if ($thisstaff->hasPerm(Task::PERM_DELETE)) { +if ($thisstaff->hasPerm(Task::PERM_DELETE, false)) { $actions += array( 'delete' => array( 'icon' => 'icon-trash', @@ -178,20 +208,46 @@ if ($thisstaff->hasPerm(Task::PERM_DELETE)) { ?> <!-- SEARCH FORM START --> <div id='basic_search'> - <form action="tasks.php" method="get"> + <div class="pull-right" style="height:25px"> + <span class="valign-helper"></span> + <span class="action-button muted" data-dropdown="#sort-dropdown"> + <i class="icon-caret-down pull-right"></i> + <span><i class="icon-sort-by-attributes-alt"></i> <?php echo __('Sort');?></span> + </span> + <div id="sort-dropdown" class="action-dropdown anchor-right" + onclick="javascript: $.pjax({ + url:'?' + addSearchParam('sort', $(event.target).data('mode')), + timeout: 2000, + container: '#pjax-container'});"> + <ul class="bleed-left"> + <?php foreach ($queue_sort_options as $mode) { + $desc = $sort_options[$mode]; + $selected = $mode == $_SESSION[$queue_sort_key]; ?> + <li <?php if ($selected) echo 'class="active"'; ?>> + <a href="#" data-mode="<?php echo $mode; ?>"><i class="icon-fixed-width <?php + if ($selected) echo 'icon-hand-right'; + ?>"></i> <?php echo Format::htmlchars($desc); ?></a> + </li> +<?php } ?> + </ul> + </div> + </div> + <form action="tasks.php" method="get" onsubmit="javascript: + $.pjax({ + url:$(this).attr('action') + '?' + $(this).serialize(), + container:'#pjax-container', + timeout: 2000 + }); +return false;"> <input type="hidden" name="a" value="search"> - <table> - <tr> - <td><input type="text" id="basic-ticket-search" name="query" - size=30 value="<?php echo Format::htmlchars($_REQUEST['query'], - true); ?>" - autocomplete="off" autocorrect="off" autocapitalize="off"></td> - <td><input type="submit" class="button" value="<?php echo __('Search'); ?>"></td> - <td> <a href="#" onclick="javascript: - $.dialog('ajax.php/tasks/search', 201);" - >[<?php echo __('advanced'); ?>]</a> <i class="help-tip icon-question-sign" href="#advanced"></i></td> - </tr> - </table> + <input type="hidden" name="search-type" value=""/> + <div class="attached input"> + <input type="text" class="basic-search" data-url="ajax.php/tasks/lookup" name="query" + autofocus size="30" value="<?php echo Format::htmlchars($_REQUEST['query'], true); ?>" + autocomplete="off" autocorrect="off" autocapitalize="off"> + <button type="submit" class="attached button"><i class="icon-search"></i> + </button> + </div> </form> </div> <!-- SEARCH FORM END --> @@ -204,20 +260,6 @@ if ($thisstaff->hasPerm(Task::PERM_DELETE)) { $results_type.$showing; ?></a></h2> </div> <div class="pull-right flush-right"> - <span style="display:inline-block"> - <span style="vertical-align: baseline">Sort:</span> - <select name="sort" onchange="javascript:addSearchParam('sort', $(this).val());"> -<?php foreach (array( - 'created' => __('Most Recently Created'), - 'updated' => __('Most Recently Updated'), - 'due' => __('Due Soon'), - 'priority,due' => __('Priority + Due Soon'), - 'number' => __('Task Number'), -) as $mode => $desc) { ?> - <option value="<?php echo $mode; ?>" <?php if ($mode == $_SESSION[$queue_sort_key]) echo 'selected="selected"'; ?>><?php echo $desc; ?></option> -<?php } ?> - </select> - </span> <?php Task::getAgentActions($thisstaff, array('status' => $status)); ?> @@ -321,7 +363,7 @@ if ($thisstaff->hasPerm(Task::PERM_DELETE)) { <?php } //end of foreach if (!$total) - $ferror=__('There are no tickets matching your criteria.'); + $ferror=__('There are no tasks matching your criteria.'); ?> </tbody> <tfoot> @@ -396,7 +438,7 @@ $(function() { var url = 'ajax.php/' +$(this).attr('href').substr(1) +'?_uid='+new Date().getTime(); - var $options = $(this).data('dialog'); + var $options = $(this).data('dialogConfig'); var $redirect = $(this).data('redirect'); $.dialog(url, [201], function (xhr) { if ($redirect) diff --git a/include/staff/templates/task-edit.tmpl.php b/include/staff/templates/task-edit.tmpl.php index b90d545f823a2e812da24300225643d69c70ed55..f86e8a70a4c751d92b23b5bf592849bb66b7c08a 100644 --- a/include/staff/templates/task-edit.tmpl.php +++ b/include/staff/templates/task-edit.tmpl.php @@ -8,6 +8,8 @@ if (!$info['title']) $action = $info['action'] ?: ('#tasks/'.$task->getId().'/edit'); +$namespace = sprintf('task.%d.edit', $task->getId()); + ?> <div id="task-form"> <h3 class="drag-handle"><?php echo $info['title']; ?></h3> @@ -24,61 +26,29 @@ if ($info['error']) { } ?> <div id="edit-task-form" style="display:block;"> <form method="post" class="task" action="<?php echo $action; ?>"> - - <table class="form_table dynamic-forms" width="100%" border="0" cellspacing="0" cellpadding="2"> - <?php if ($forms) - foreach ($forms as $form) { - $form->render(true, false, array('mode'=>'edit','width'=>160,'entry'=>$form)); - print $form->getForm()->getMedia(); - } ?> - </table> - <table class="form_table dynamic-forms" width="100%" border="0" cellspacing="0" cellpadding="2"> - <tr><th colspan=2><em><?php - echo __('Task Visibility & Assignment'); ?></em></th></tr> + <div> <?php - $iform = $iform ?: TaskForm::getInternalForm(); - foreach ($iform->getFields() as $name=>$field) { - if (!$field->isEditable()) continue; - ?> - <tr> - <td class="multi-line <?php if ($field->get('required')) echo 'required'; - ?>" style="min-width:120px;" width="160"> - <?php echo Format::htmlchars($field->get('label')); ?>:</td> - <td> - <fieldset id="field<?php echo $field->getWidget()->id; - ?>" <?php if (!$field->isVisible()) echo 'style="display:none;"'; ?>> - <?php echo $field->render(); ?> - <?php if ($field->get('required')) { ?> - <span class="error">*</span> - <?php - } - foreach ($field->errors() as $E) { - ?><div class="error"><?php echo $E; ?></div><?php - } ?> - </fieldset> - </td> - </tr> - <?php - } - ?> - </table> - <table class="form_table" width="100%" border="0" cellspacing="0" cellpadding="2"> - <tbody> - <tr> - <th colspan="2"> - <em><strong><?php echo __('Internal Note');?></strong>: <?php - echo __('Reason for editing the task (optional');?> <font class="error"> <?php echo $errors['note'];?></font></em> - </th> - </tr> - <tr> - <td colspan="2"> - <textarea class="richtext no-bar" name="note" cols="21" - rows="6" style="width:80%;"><?php echo $info['note']; - ?></textarea> - </td> - </tr> - </tbody> - </table> + if ($forms) { + foreach ($forms as $form) + echo $form->getForm(false, array('mode' => 'edit'))->asTable( + __('Task Information'), + array( + 'draft-namespace' => $namespace, + ) + ); + } + ?> + </div> + <div><strong><?php echo __('Internal Note');?></strong>: + <font class="error"> <?php echo $errors['note'];?></font></div> + <div> + <textarea class="richtext no-bar" name="note" cols="21" rows="6" + style="width:80%;" + placeholder="<?php echo __('Reason for editing the task (optional)'); ?>" + > + <?php echo $info['note']; + ?></textarea> + </div> <hr> <p class="full-width"> <span class="buttons pull-left"> diff --git a/include/staff/templates/task-view.tmpl.php b/include/staff/templates/task-view.tmpl.php index a72674066dda6442b29d01709e30c3d9b562823e..e3d577c870c17fe258e9757abbffde34d2dc9bb0 100644 --- a/include/staff/templates/task-view.tmpl.php +++ b/include/staff/templates/task-view.tmpl.php @@ -4,6 +4,8 @@ if (!defined('OSTSCPINC') || !($role = $thisstaff->getRole($task->getDeptId()))) die('Invalid path'); +global $cfg; + $actions = array(); $actions += array( @@ -54,7 +56,10 @@ if ($role->hasPerm(Task::PERM_DELETE)) { $info=($_POST && $errors)?Format::input($_POST):array(); -$id = $task->getId(); +$id = $task->getId(); +$dept = $task->getDept(); +$thread = $task->getThread(); + if ($task->isOverdue()) $warn.=' <span class="Icon overdueTicket">'.__('Marked overdue!').'</span>'; @@ -67,7 +72,7 @@ if ($task->isOverdue()) <?php if ($ticket) { ?> <strong> - <a id="ticket-tasks" href="#"> All Tasks (<?php echo $ticket->getNumTasks(); ?>)</a> + <a id="all-ticket-tasks" href="#"> All Tasks (<?php echo $ticket->getNumTasks(); ?>)</a> / <?php echo $task->getTitle(); ?> — @@ -76,7 +81,9 @@ if ($task->isOverdue()) <?php echo ' class="preview" '; echo sprintf('data-preview="#tasks/%d/preview" ', $task->getId()); - echo sprintf('href="#tasks/%d" ', $task->getId()); + echo sprintf('href="#tickets/%s/tasks/%d/view" ', + $ticket->getId(), $task->getId() + ); ?> ><?php echo sprintf('#%s', $task->getNumber()); ?></a> @@ -98,6 +105,12 @@ if ($task->isOverdue()) <div class="flush-right"> <?php if ($ticket) { ?> + <a id="task-view" + target="_blank" + class="action-button" + href="tasks.php?id=<?php + echo $task->getId(); ?>"><i class="icon-share"></i> <?php + echo __('View Task'); ?></a> <span class="action-button" data-dropdown="#action-dropdown-task-options"> @@ -116,7 +129,7 @@ if ($task->isOverdue()) echo $action['class'] ?: 'task-action'; ?>" <?php if ($action['dialog']) - echo sprintf("data-dialog='%s'", $action['dialog']); + echo sprintf("data-dialog-config='%s'", $action['dialog']); if ($action['redirect']) echo sprintf("data-redirect='%s'", $action['redirect']); ?> @@ -136,7 +149,7 @@ if ($task->isOverdue()) echo $action['class'] ?: 'task-action'; ?>" <?php if ($action['dialog']) - echo sprintf("data-dialog='%s'", $action['dialog']); + echo sprintf("data-dialog-config='%s'", $action['dialog']); ?> href="<?php echo $action['href']; ?>"><i class="<?php @@ -308,10 +321,104 @@ if ($ticket) else $action = 'tasks.php?id='.$task->getId(); ?> -<div id="response_options" class="sticky bar stop"> - <ul class="tabs"></ul> - <form id="<?php echo $ticket? 'ticket_task_note': 'task_note'; ?>" +<div id="task_response_options" class="<?php echo $ticket ? 'ticket_task_actions' : ''; ?> sticky bar stop"> + <ul class="tabs"> + <?php + if ($role->hasPerm(TaskModel::PERM_REPLY)) { ?> + <li class="active"><a href="#task_reply"><?php echo __('Post Update');?></a></li> + <li><a href="#task_note"><?php echo __('Post Internal Note');?></a></li> + <?php + }?> + </ul> + <?php + if ($role->hasPerm(TaskModel::PERM_REPLY)) { ?> + <form id="task_reply" class="tab_content spellcheck" + action="<?php echo $action; ?>" + name="task_reply" method="post" enctype="multipart/form-data"> + <?php csrf_token(); ?> + <input type="hidden" name="id" value="<?php echo $task->getId(); ?>"> + <input type="hidden" name="a" value="postreply"> + <input type="hidden" name="lockCode" value="<?php echo ($mylock) ? $mylock->getCode() : ''; ?>"> + <span class="error"></span> + <table style="width:100%" border="0" cellspacing="0" cellpadding="3"> + <tbody id="collab_sec" style="display:table-row-group"> + <tr> + <td> + <input type='checkbox' value='1' name="emailcollab" id="emailcollab" + <?php echo ((!$info['emailcollab'] && !$errors) || isset($info['emailcollab']))?'checked="checked"':''; ?> + style="display:<?php echo $thread->getNumCollaborators() ? 'inline-block': 'none'; ?>;" + > + <?php + $recipients = __('Add Participants'); + if ($thread->getNumCollaborators()) + $recipients = sprintf(__('Recipients (%d of %d)'), + $thread->getNumActiveCollaborators(), + $thread->getNumCollaborators()); + + echo sprintf('<span><a class="collaborators preview" + href="#thread/%d/collaborators"><span id="t%d-recipients">%s</span></a></span>', + $thread->getId(), + $thread->getId(), + $recipients); + ?> + </td> + </tr> + </tbody> + <tbody id="update_sec"> + <tr> + <td> + <div class="error"><?php echo $errors['response']; ?></div> + <input type="hidden" name="draft_id" value=""/> + <textarea name="response" id="task-response" cols="50" + data-signature-field="signature" data-dept-id="<?php echo $dept->getId(); ?>" + data-signature="<?php + echo Format::htmlchars(Format::viewableImages($signature)); ?>" + placeholder="<?php echo __( 'Start writing your update here.'); ?>" + rows="9" wrap="soft" + class="<?php if ($cfg->isRichTextEnabled()) echo 'richtext'; + ?> draft draft-delete" <?php + list($draft, $attrs) = Draft::getDraftAndDataAttrs('task.response', $task->getId(), $info['task.response']); + echo $attrs; ?>><?php echo $draft ?: $info['task.response']; + ?></textarea> + <div id="task_response_form_attachments" class="attachments"> + <?php + if ($reply_attachments_form) + print $reply_attachments_form->getField('attachments')->render(); + ?> + </div> + </td> + </tr> + <tr> + <td> + <div><?php echo __('Status');?> + <span class="faded"> - </span> + <select name="task_status"> + <option value="1" <?php + echo $task->isOpen() ? + 'selected="selected"': ''; ?>> <?php + echo _('Open'); ?></option> + <option value="0" <?php + echo $task->isClosed() ? + 'selected="selected"': ''; ?>> <?php + echo _('Closed'); ?></option> + </select> + <span class='error'><?php echo + $errors['task_status']; ?></span> + </div> + </td> + </tr> + </table> + <p style="padding-left:165px;"> + <input class="btn_sm" type="submit" value="<?php echo __('Post Update');?>"> + <input class="btn_sm" type="reset" value="<?php echo __('Reset');?>"> + </p> + </form> + <?php + } ?> + <form id="task_note" action="<?php echo $action; ?>" + class="tab_content spellcheck <?php + echo $role->hasPerm(TaskModel::PERM_REPLY) ? 'hidden' : ''; ?>" name="task_note" method="post" enctype="multipart/form-data"> <?php csrf_token(); ?> @@ -320,18 +427,9 @@ else <table width="100%" border="0" cellspacing="0" cellpadding="3"> <tr> <td> - <div> - <div class="faded" style="padding-left:0.15em"><?php - echo __('Note title - summary of the note (optional)'); ?></div> - <input type="text" name="title" id="title" size="60" value="<?php echo $info['title']; ?>" > - <br/> - <span class="error"> <?php echo $errors['title']; ?></span> - </div> - <div> - <label><strong><?php echo __('Internal Note'); ?></strong><span class='error'> * <?php echo $errors['note']; ?></span></label> - </div> - <textarea name="note" id="internal_note" cols="80" - placeholder="<?php echo __('Note details'); ?>" + <div><span class='error'><?php echo $errors['note']; ?></span></div> + <textarea name="note" id="task-note" cols="80" + placeholder="<?php echo __('Internal Note details'); ?>" rows="9" wrap="soft" data-draft-namespace="task.note" data-draft-object-id="<?php echo $task->getId(); ?>" class="richtext ifhtml draft draft-delete"><?php @@ -339,15 +437,15 @@ else ?></textarea> <div class="attachments"> <?php - if ($note_form) - print $note_form->getField('attachments')->render(); + if ($note_attachments_form) + print $note_attachments_form->getField('attachments')->render(); ?> </div> </td> </tr> <tr> <td> - <div><?php echo __('Task Status');?> + <div><?php echo __('Status');?> <span class="faded"> - </span> <select name="task_status"> <option value="1" <?php @@ -371,11 +469,14 @@ else </p> </form> </div> +<?php +echo $reply_attachments_form->getMedia(); +?> <script type="text/javascript"> $(function() { $(document).off('.tasks-content'); - $(document).on('click.tasks-content', 'a#ticket-tasks', function(e) { + $(document).on('click.tasks-content', '#all-ticket-tasks', function(e) { e.preventDefault(); $('div#task_content').hide().empty(); $('div#tasks_content').show(); @@ -388,7 +489,7 @@ $(function() { var url = 'ajax.php/' +$(this).attr('href').substr(1) +'?_uid='+new Date().getTime(); - var $options = $(this).data('dialog'); + var $options = $(this).data('dialogConfig'); var $redirect = $(this).data('redirect'); $.dialog(url, [201], function (xhr) { if ($redirect) @@ -401,7 +502,7 @@ $(function() { }); $(document).off('.tf'); - $(document).on('submit.tf', 'form#ticket_task_note', function(e) { + $(document).on('submit.tf', '.ticket_task_actions form', function(e) { e.preventDefault(); var $form = $(this); var $container = $('div#task_content'); diff --git a/include/staff/templates/task.tmpl.php b/include/staff/templates/task.tmpl.php index 1271318ba3a5e48b7715ec5577987882e5fa1056..7bcafc77d68a267e0a20f62fdf8480f5edec29b0 100644 --- a/include/staff/templates/task.tmpl.php +++ b/include/staff/templates/task.tmpl.php @@ -23,41 +23,15 @@ if ($info['error']) { } ?> <div id="new-task-form" style="display:block;"> <form method="post" class="org" action="<?php echo $info['action'] ?: '#tasks/add'; ?>"> - <table width="100%" class="fixed"> <?php $form = $form ?: TaskForm::getInstance(); - $form->render(true, - __('Create New Task'), + echo $form->getForm()->asTable(' ', array('draft-namespace' => $namespace) ); - ?> - <tr><th colspan=2><em><?php - echo __('Task Visibility & Assignment'); ?></em></th></tr> - <?php + $iform = $iform ?: TaskForm::getInternalForm(); - foreach ($iform->getFields() as $name=>$field) { ?> - <tr> - <td class="multi-line <?php if ($field->get('required')) echo 'required'; - ?>" style="min-width:120px;" > - <?php echo Format::htmlchars($field->get('label')); ?>:</td> - <td> - <fieldset id="field<?php echo $field->getWidget()->id; - ?>" <?php if (!$field->isVisible()) echo 'style="display:none;"'; ?>> - <?php echo $field->render(); ?> - <?php if ($field->get('required')) { ?> - <span class="error">*</span> - <?php - } - foreach ($field->errors() as $E) { - ?><div class="error"><?php echo $E; ?></div><?php - } ?> - </fieldset> - </td> - </tr> - <?php - } - ?> - </table> + echo $iform->asTable(__("Task Visibility & Assignment")); +?> <hr> <p class="full-width"> <span class="buttons pull-left"> diff --git a/include/staff/templates/tasks-actions.tmpl.php b/include/staff/templates/tasks-actions.tmpl.php index 306cd6ea9ab29c390f4dd01c3bca570890b7a2fe..e5407d2ed902e702d023fda00ba497842d35f8a6 100644 --- a/include/staff/templates/tasks-actions.tmpl.php +++ b/include/staff/templates/tasks-actions.tmpl.php @@ -3,7 +3,7 @@ $actions = array(); -if ($agent->hasPerm(Task::PERM_CLOSE)) { +if ($agent->hasPerm(Task::PERM_CLOSE, false)) { if (isset($options['status'])) { $status = $options['status']; @@ -59,7 +59,7 @@ if ($agent->hasPerm(Task::PERM_CLOSE)) { } } -if ($agent->hasPerm(Task::PERM_ASSIGN)) { +if ($agent->hasPerm(Task::PERM_ASSIGN, false)) { $actions += array( 'assign' => array( 'icon' => 'icon-user', @@ -67,7 +67,7 @@ if ($agent->hasPerm(Task::PERM_ASSIGN)) { )); } -if ($agent->hasPerm(Task::PERM_TRANSFER)) { +if ($agent->hasPerm(Task::PERM_TRANSFER, false)) { $actions += array( 'transfer' => array( 'icon' => 'icon-share', @@ -75,7 +75,7 @@ if ($agent->hasPerm(Task::PERM_TRANSFER)) { )); } -if ($agent->hasPerm(Task::PERM_DELETE)) { +if ($agent->hasPerm(Task::PERM_DELETE, false)) { $actions += array( 'delete' => array( 'icon' => 'icon-trash', @@ -102,7 +102,7 @@ if ($actions) { <a class="no-pjax tasks-action" <?php if ($action['dialog']) - echo sprintf("data-dialog='%s'", $action['dialog']); + echo sprintf("data-dialog-config='%s'", $action['dialog']); if ($action['redirect']) echo sprintf("data-redirect='%s'", $action['redirect']); ?> @@ -120,8 +120,8 @@ if ($actions) { } ?> <script type="text/javascript"> $(function() { - $(document).off('.tasks'); - $(document).on('click.tasks', 'a.tasks-action', function(e) { + $(document).off('.tasks-actions'); + $(document).on('click.tasks-actions', 'a.tasks-action', function(e) { e.preventDefault(); var count = checkbox_checker($('form#tasks'), 1); if (count) { diff --git a/include/staff/templates/thread-entry-edit.tmpl.php b/include/staff/templates/thread-entry-edit.tmpl.php index 1440780061aa0831d3009522a54f501c63425ca4..3f2d0fe56235af4ab6e539da566dc6ba578ce1ee 100644 --- a/include/staff/templates/thread-entry-edit.tmpl.php +++ b/include/staff/templates/thread-entry-edit.tmpl.php @@ -12,8 +12,9 @@ <?php if ($poster && $this->entry->type == 'R') { $signature_type = $poster->getDefaultSignatureType(); $signature = ''; - if (($T = $this->entry->getThread()->getObject()) instanceof Ticket) + if (($T = $this->entry->getThread()->getObject())) $dept = $T->getDept(); + switch ($poster->getDefaultSignatureType()) { case 'dept': if ($dept && $dept->canAppendSignature()) @@ -24,7 +25,7 @@ $signature_type = 'theirs'; break; } ?> - data-dept-id="<?php echo $dept->getId(); ?>" + data-dept-id="<?php echo $dept ? $dept->getId() : 0; ?>" data-poster-id="<?php echo $this->entry->staff_id; ?>" data-signature-field="signature" data-signature="<?php echo Format::htmlchars(Format::viewableImages($signature)); ?>" diff --git a/include/staff/ticket-tasks.inc.php b/include/staff/ticket-tasks.inc.php index 84300a10d52a1ebb1d416bfa97670a7cf8c2f215..824ebb1542b65ab9c49c47d9b445bbff3b2dd0db 100644 --- a/include/staff/ticket-tasks.inc.php +++ b/include/staff/ticket-tasks.inc.php @@ -31,9 +31,9 @@ $showing = $pageNav->showing().' '._N('task', 'tasks', $count); <?php if ($role && $role->hasPerm(Task::PERM_CREATE)) { ?> <a - class="action-button ticket-task-action" + class="green button action-button ticket-task-action" data-url="tickets.php?id=<?php echo $ticket->getId(); ?>#tasks" - data-dialog='{"size":"large"}' + data-dialog-config='{"size":"large"}' href="#tickets/<?php echo $ticket->getId(); ?>/add-task"> <i class="icon-plus-sign"></i> <?php @@ -133,9 +133,10 @@ if ($count) { ?> <script type="text/javascript"> $(function() { - $(document).off('click.tasks'); - $(document).on('click.tasks', 'tbody.tasks a, a#reload-task', function(e) { + $(document).off('click.taskv'); + $(document).on('click.taskv', 'tbody.tasks a, a#reload-task', function(e) { e.preventDefault(); + e.stopImmediatePropagation(); var url = 'ajax.php/'+$(this).attr('href').substr(1); var $container = $('div#task_content'); var $stop = $('ul#ticket_tabs').offset().top; @@ -146,6 +147,7 @@ $(function() { $('.tip_box').remove(); $('div#tasks_content').hide(); }); + return false; }); // Ticket Tasks @@ -156,7 +158,7 @@ $(function() { +$(this).attr('href').substr(1) +'?_uid='+new Date().getTime(); var $redirect = $(this).data('href'); - var $options = $(this).data('dialog'); + var $options = $(this).data('dialogConfig'); $.dialog(url, [201], function (xhr) { var tid = parseInt(xhr.responseText); if (tid) { @@ -172,7 +174,5 @@ $(function() { }, $options); return false; }); - - }); </script> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 695ac4d745a312fbe8a23042c8ef4e254d549f20..d037930f342b6b0f0d4cd495056ba48073d01bdc 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -417,7 +417,7 @@ $tcount = $ticket->getThreadEntries($types)->count(); ?> <ul class="tabs clean threads" id="ticket_tabs" > <li class="active"><a href="#ticket_thread"><?php echo sprintf(__('Ticket Thread (%d)'), $tcount); ?></a></li> - <li><a id="ticket_tasks" href="#tasks" + <li><a id="ticket-tasks-tab" href="#tasks" data-url="<?php echo sprintf('#tickets/%d/tasks', $ticket->getId()); ?>"><?php echo __('Tasks'); diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index 8d6c2bc4a8135ba38bb56ed85bbbe0b844971481..4a6519410d51d8e127c96f6fdca6acf1e4fe05e1 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -192,7 +192,7 @@ $pageNav->setURL('tickets.php', $args); $tickets = $pageNav->paginate($tickets); // Apply requested sorting -$queue_sort_key = sprintf(':Q:%s:sort', $queue_name); +$queue_sort_key = sprintf(':Q%s:%s:sort', ObjectModel::OBJECT_TYPE_TICKET, $queue_name); if (isset($_GET['sort'])) { $_SESSION[$queue_sort_key] = $_GET['sort']; @@ -316,7 +316,7 @@ $_SESSION[':Q:tickets'] = $orig_tickets; <span><i class="icon-sort-by-attributes-alt"></i> <?php echo __('Sort');?></span> </span> <div id="sort-dropdown" class="action-dropdown anchor-right" - onclick="javascript: console.log(event); $.pjax({ + onclick="javascript: $.pjax({ url:'?' + addSearchParam('sort', $(event.target).data('mode')), timeout: 2000, container: '#pjax-container'});"> @@ -325,7 +325,7 @@ $_SESSION[':Q:tickets'] = $orig_tickets; $desc = $sort_options[$mode]; $selected = $mode == $_SESSION[$queue_sort_key]; ?> <li <?php if ($selected) echo 'class="active"'; ?>> - <a data-mode="<?php echo $mode; ?>"><i class="icon-fixed-width <?php + <a href="#" data-mode="<?php echo $mode; ?>"><i class="icon-fixed-width <?php if ($selected) echo 'icon-hand-right'; ?>"></i> <?php echo Format::htmlchars($desc); ?></a> </li> @@ -343,7 +343,7 @@ return false;"> <input type="hidden" name="a" value="search"> <input type="hidden" name="search-type" value=""/> <div class="attached input"> - <input type="text" id="basic-ticket-search" name="query" + <input type="text" class="basic-search" data-url="ajax.php/tickets/lookup" name="query" autofocus size="30" value="<?php echo Format::htmlchars($_REQUEST['query'], true); ?>" autocomplete="off" autocorrect="off" autocapitalize="off"> <button type="submit" class="attached button"><i class="icon-search"></i> diff --git a/include/upgrader/streams/core/0d6099a6-98ad7d55.patch.sql b/include/upgrader/streams/core/0d6099a6-98ad7d55.patch.sql index ab6a3f7576c1cd4b8a633c503e4051334b04057e..35c654582659f8bb30eaddcc9fd1eef35eb68422 100644 --- a/include/upgrader/streams/core/0d6099a6-98ad7d55.patch.sql +++ b/include/upgrader/streams/core/0d6099a6-98ad7d55.patch.sql @@ -43,6 +43,13 @@ UPDATE `%TABLE_PREFIX%thread` A1 SET A1.`lastresponse` = A2.`lastresponse`, A1.`lastmessage` = A2.`lastmessage`; +-- Mark `message` field as externally stored +-- DynamicFormField::FLAG_EXT_STORED = 0x00002; +UPDATE `%TABLE_PREFIX%form_field` A1 + JOIN `%TABLE_PREFIX%form` A2 ON (A2.`id` = A1.`form_id`) + SET A1.`flags` = A1.`flags` | 0x00002 + WHERE A2.`type` = 'T' AND A1.`name` = 'message'; + -- Finished with patch UPDATE `%TABLE_PREFIX%config` SET `value` = '98ad7d550c26ac44340350912296e673' diff --git a/include/upgrader/streams/core/b26f29a6-1ee831c8.patch.sql b/include/upgrader/streams/core/b26f29a6-1ee831c8.patch.sql index 4eb98233d7ff69fad3fbafdcdb573b0304715a87..84df9fc92396ad1169464db4aa637c135d98cf20 100644 --- a/include/upgrader/streams/core/b26f29a6-1ee831c8.patch.sql +++ b/include/upgrader/streams/core/b26f29a6-1ee831c8.patch.sql @@ -250,13 +250,6 @@ UPDATE `%TABLE_PREFIX%ticket_attachment` A1 DROP TABLE `%TABLE_PREFIX%_unknown_inlines`; --- Mark `message` field as externally stored --- DynamicFormField::FLAG_EXT_STORED = 0x00002; -UPDATE `%TABLE_PREFIX%form_field` A1 - JOIN `%TABLE_PREFIX%form` A2 ON (A2.`id` = A1.`form_id`) - SET A1.`flags` = A1.`flags` | 0x00002 - WHERE A2.`type` = 'T' AND A1.`name` = 'message'; - -- Finished with patch UPDATE `%TABLE_PREFIX%config` SET `value` = '1ee831c854fe9f35115a3e672916bb91' diff --git a/scp/ajax.php b/scp/ajax.php index be0e7c33db7ca4dfabef4b5f1d5087f164551e00..bdaf0a45483e7548617fd0c3715493f738ba130e 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -181,6 +181,7 @@ $dispatcher = patterns('', url_get('^(?P<tid>\d+)/view$', 'task'), url_post('^(?P<tid>\d+)$', 'task'), url('^add$', 'add'), + url('^lookup', 'lookup'), url_get('^mass/(?P<action>[\w.]+)', 'massProcess'), url_post('^mass/(?P<action>[\w.]+)', 'massProcess') )), diff --git a/scp/js/scp.js b/scp/js/scp.js index fee30477431bd42982dc70250182d34af6c8f5bb..a97e1ef94ce5ae7e3688b72b97bdc5fd537f79f7 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -252,11 +252,13 @@ var scp_prep = function() { /* Typeahead tickets lookup */ var last_req; - $('#basic-ticket-search').typeahead({ + $('input.basic-search').typeahead({ source: function (typeahead, query) { if (last_req) last_req.abort(); + var $el = this.$element; + var url = $el.data('url')+'?q='+query; last_req = $.ajax({ - url: "ajax.php/tickets/lookup?q="+query, + url: url, dataType: 'json', success: function (data) { typeahead.process(data); @@ -264,9 +266,10 @@ var scp_prep = function() { }); }, onselect: function (obj) { - var form = $('#basic-ticket-search').closest('form'); + var $el = this.$element; + var form = $el.closest('form'); form.find('input[name=search-type]').val('typeahead'); - $('#basic-ticket-search').val(obj.value); + $el.val(obj.value); form.submit(); }, property: "matches" @@ -756,7 +759,7 @@ $.orgLookup = function (url, cb) { $.uid = 1; // Tabs -$(document).on('click.tab', 'ul.tabs li a', function(e) { +$(document).on('click.tab', 'ul.tabs > li > a', function(e) { e.preventDefault(); var $this = $(this), $ul = $(this).closest('ul'), @@ -777,6 +780,7 @@ $(document).on('click.tab', 'ul.tabs li a', function(e) { // TODO: Add / hide loading spinner }) ); + $this.removeData('url'); } else { $tab.addClass('tab_content'); diff --git a/scp/staff.inc.php b/scp/staff.inc.php index edbc813006964649e43d1ed6e40cc03282b9fda0..bb06f3eb8e92dcf8094b1f1500137c282eddceec 100644 --- a/scp/staff.inc.php +++ b/scp/staff.inc.php @@ -76,7 +76,7 @@ if (!$thisstaff || !$thisstaff->getId() || !$thisstaff->isValid()) { //2) if not super admin..check system status and group status if(!$thisstaff->isAdmin()) { //Check for disabled staff or group! - if(!$thisstaff->isactive() || !$thisstaff->isGroupActive()) { + if (!$thisstaff->isactive()) { staffLoginPage(__('Access Denied. Contact Admin')); exit; } diff --git a/scp/tasks.php b/scp/tasks.php index c22fa5bdb39671a5e47e835d8a8447711b022f6a..ced34f872ad4bd73cd9e75af307393f5c31b84ef 100644 --- a/scp/tasks.php +++ b/scp/tasks.php @@ -13,6 +13,7 @@ require('staff.inc.php'); require_once(INCLUDE_DIR.'class.task.php'); +require_once(INCLUDE_DIR.'class.export.php'); $page = ''; $task = null; //clean start. @@ -26,12 +27,18 @@ if ($_REQUEST['id']) { } // Configure form for file uploads -$note_form = new SimpleForm(array( +$note_attachments_form = new SimpleForm(array( 'attachments' => new FileUploadField(array('id'=>'attach', 'name'=>'attach:note', 'configuration' => array('extensions'=>''))) )); +$reply_attachments_form = new SimpleForm(array( + 'attachments' => new FileUploadField(array('id'=>'attach', + 'name'=>'attach:reply', + 'configuration' => array('extensions'=>''))) +)); + //At this stage we know the access status. we can process the post. if($_POST && !$errors): @@ -42,7 +49,7 @@ if($_POST && !$errors): switch(strtolower($_POST['a'])): case 'postnote': /* Post Internal Note */ $vars = $_POST; - $attachments = $note_form->getField('attachments')->getClean(); + $attachments = $note_attachments_form->getField('attachments')->getClean(); $vars['cannedattachments'] = array_merge( $vars['cannedattachments'] ?: array(), $attachments); @@ -51,24 +58,51 @@ if($_POST && !$errors): $msg=__('Internal note posted successfully'); // Clear attachment list - $note_form->setSource(array()); - $note_form->getField('attachments')->reset(); + $note_attachments_form->setSource(array()); + $note_attachments_form->getField('attachments')->reset(); if($wasOpen && $task->isClosed()) $task = null; //Going back to main listing. else - // Ticket is still open -- clear draft for the note + // Task is still open -- clear draft for the note Draft::deleteForNamespace('task.note.'.$task->getId(), $thisstaff->getId()); } else { - if(!$errors['err']) $errors['err'] = __('Unable to post internal note - missing or invalid data.'); $errors['postnote'] = __('Unable to post the note. Correct the error(s) below and try again!'); } break; + case 'postreply': /* Post an update */ + $vars = $_POST; + $attachments = $reply_attachments_form->getField('attachments')->getClean(); + $vars['cannedattachments'] = array_merge( + $vars['cannedattachments'] ?: array(), $attachments); + + $wasOpen = ($task->isOpen()); + if (($response=$task->postReply($vars, $errors))) { + + $msg=__('Reply posted successfully'); + // Clear attachment list + $reply_attachments_form->setSource(array()); + $reply_attachments_form->getField('attachments')->reset(); + + if ($wasOpen && $task->isClosed()) + $task = null; //Going back to main listing. + else + // Task is still open -- clear draft for the note + Draft::deleteForNamespace('task.reply.'.$task->getId(), + $thisstaff->getId()); + + } else { + if (!$errors['err']) + $errors['err'] = __('Unable to post reply - missing or invalid data.'); + + $errors['postreply'] = __('Unable to post the reply. Correct the error(s) below and try again!'); + } + break; default: $errors['err']=__('Unknown action'); endswitch; @@ -139,14 +173,14 @@ if (isset($_SESSION['advsearch:tasks'])) { (!$_REQUEST['status'] || $_REQUEST['status']=='search')); } -if ($thisstaff->hasPerm(TaskModel::PERM_CREATE)) { +if ($thisstaff->hasPerm(TaskModel::PERM_CREATE, false)) { $nav->addSubMenu(array('desc'=>__('New Task'), 'title'=> __('Open a New Task'), 'href'=>'#tasks/add', 'iconclass'=>'newTicket task-action', 'id' => 'new-task', 'attr' => array( - 'data-dialog' => '{"size":"large"}' + 'data-dialog-config' => '{"size":"large"}' ) ), ($_REQUEST['a']=='open')); @@ -173,7 +207,7 @@ if($task) { } else { $inc = 'tasks.inc.php'; if ($_REQUEST['a']=='open' && - $thisstaff->hasPerm(Task::PERM_CREATE)) + $thisstaff->hasPerm(Task::PERM_CREATE, false)) $inc = 'task-open.inc.php'; elseif($_REQUEST['a'] == 'export') { $ts = strftime('%Y%m%d'); diff --git a/setup/inc/class.installer.php b/setup/inc/class.installer.php index 3986191a94c517f3cbdb39be38b00cc4081bf1cf..6ab645e7d6cdbe4da784f2b0396395cac2eb0d10 100644 --- a/setup/inc/class.installer.php +++ b/setup/inc/class.installer.php @@ -199,7 +199,7 @@ class Installer extends SetupWizard { Organization::PERM_DELETE, FAQ::PERM_MANAGE, Email::PERM_BANLIST, - ), $errors); + )); $staff->setPassword($vars['passwd']); if (!$staff->save()) { $this->errors['err'] = __('Unable to create admin user (#6)');