diff --git a/include/ajax.forms.php b/include/ajax.forms.php index 9ca601e33020d9c0f3ea1e669bbc4d405217df5e..c25570e095754f8e4237de353c817aae8259269e 100644 --- a/include/ajax.forms.php +++ b/include/ajax.forms.php @@ -32,7 +32,8 @@ class DynamicFormsAjaxAPI extends AjaxController { if (!$form->hasAnyVisibleFields()) continue; ob_start(); - $form->getForm($_SESSION[':form-data'])->render(!$client); + $form->getForm($_SESSION[':form-data'])->render(!$client, false, + array('mode' => 'create')); $html .= ob_get_clean(); ob_start(); print $form->getMedia(); diff --git a/include/ajax.orgs.php b/include/ajax.orgs.php index 9c2a2b207c11ba72e8b6c7fa259670d0b4b6a0d7..d3ffc8b9dec2b994ae95efd133104a4860d308dc 100644 --- a/include/ajax.orgs.php +++ b/include/ajax.orgs.php @@ -93,7 +93,7 @@ class OrgsAjaxAPI extends AjaxController { $errors = array(); if($org->update($_POST, $errors)) - Http::response(201, $org->to_json()); + Http::response(201, $org->to_json(), 'application/json'); $forms = $org->getForms(); @@ -162,7 +162,7 @@ class OrgsAjaxAPI extends AjaxController { } if (!$info['error'] && $user && $user->setOrganization($org)) - Http::response(201, $user->to_json()); + Http::response(201, $user->to_json(), 'application/json'); elseif (!$info['error']) $info['error'] = sprintf('%s - %s', __('Unable to add user to the organization'), __('Please try again!')); @@ -230,7 +230,7 @@ class OrgsAjaxAPI extends AjaxController { if ($_POST) { $form = OrganizationForm::getDefaultForm()->getForm($_POST); if (($org = Organization::fromForm($form))) - Http::response(201, $org->to_json()); + Http::response(201, $org->to_json(), 'application/json'); $info = array('error' =>sprintf('%s - %s', __('Error adding organization'), __('Please try again!'))); } @@ -274,7 +274,7 @@ class OrgsAjaxAPI extends AjaxController { $info += array('title' => __('Organization Lookup')); if ($_POST && ($org = Organization::lookup($_POST['orgid']))) { - Http::response(201, $org->to_json()); + Http::response(201, $org->to_json(), 'application/json'); } ob_start(); diff --git a/include/ajax.search.php b/include/ajax.search.php index fabe2d810fdd8e694d566b418b656c028e94a916..8ac463aa0e247f2f7ddef5c85915848494464731 100644 --- a/include/ajax.search.php +++ b/include/ajax.search.php @@ -394,9 +394,8 @@ class SearchAjaxAPI extends AjaxController { $criteria = array(); if ($ids && is_array($ids)) $criteria = array('id__in' => $ids); - - $counts = SavedQueue::counts($thisstaff, $criteria); + $counts = SavedQueue::counts($thisstaff, true, $criteria); Http::response(200, false, 'application/json'); - return $this->encode($counts); + return $this->encode(Format::number($counts)); } } diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 2864efda74a09e4c5ceed50e699f15edbff6f5ef..3338ea4c536f334dcf73df470ecb62d2182b6e37 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -1256,6 +1256,70 @@ function refer($tid, $target=null) { return self::_changeSelectedTicketsStatus($state, $info, $errors); } + function markAs($tid, $action='') { + global $thisstaff; + + // Standard validation + if (!($ticket=Ticket::lookup($tid))) + Http::response(404, __('No such ticket')); + + if (!$ticket->checkStaffPerm($thisstaff, Ticket::PERM_REPLY) && !$thisstaff->isManager()) + Http::response(403, __('Permission denied')); + + $errors = array(); + $info = array(':title' => __('Please Confirm')); + + // Instantiate form for comment field + $form = MarkAsForm::instantiate($_POST); + + // Mark as answered or unanswered + if ($_POST) { + switch($action) { + case 'answered': + if($ticket->isAnswered()) + $errors['err'] = __('Ticket is already marked as answered'); + elseif (!$ticket->markAnswered()) + $errors['err'] = __('Cannot mark ticket as answered'); + break; + + case 'unanswered': + if(!$ticket->isAnswered()) + $errors['err'] = __('Ticket is already marked as unanswered'); + elseif (!$ticket->markUnanswered()) + $errors['err'] - __('Cannot mark ticket as unanswered'); + break; + + default: + Http::response(404, __('Unknown action')); + } + + // Retrun errors to form (if any) + if($errors) { + $info['error'] = $errors['err'] ?: sprintf(__('Unable to mark ticket as %s'), $action); + $form->addErrors($errors); + } else { + // Add comment (if provided) + $comments = $form->getComments(); + if ($comments) { + $title = __(sprintf('Ticket Marked %s', ucfirst($action))); + $_errors = array(); + + $ticket->postNote( + array('note' => $comments, 'title' => $title), + $_errors, $thisstaff, false); + } + + // Add success messages and log activity + $_SESSION['::sysmsgs']['msg'] = sprintf(__('Ticket marked as %s successfully'), $action); + $msg = sprintf(__('Ticket flagged as %s by %s'), $action, $thisstaff->getName()); + $ticket->logActivity(sprintf(__('Ticket Marked %s'), ucfirst($action)), $msg); + Http::response(201, $ticket->getId()); + } + } + + include STAFFINC_DIR . 'templates/mark-as.tmpl.php'; + } + function triggerThreadAction($ticket_id, $thread_id, $action) { $thread = ThreadEntry::lookup($thread_id); if (!$thread) diff --git a/include/ajax.users.php b/include/ajax.users.php index 328b93cf0e1d1167797ad29fcda7a0d6a1b8da8b..459c4654286a5ad2bac46bcf1aba222caaf2f380 100644 --- a/include/ajax.users.php +++ b/include/ajax.users.php @@ -168,7 +168,7 @@ class UsersAjaxAPI extends AjaxController { $errors = array(); if ($user->updateInfo($_POST, $errors, true) && !$errors) - Http::response(201, $user->to_json()); + Http::response(201, $user->to_json(), 'application/json'); $forms = $user->getForms(); include(STAFFINC_DIR . 'templates/user.tmpl.php'); @@ -271,7 +271,7 @@ class UsersAjaxAPI extends AjaxController { function getUser($id=false) { if(($user=User::lookup(($id) ? $id : $_REQUEST['id']))) - Http::response(201, $user->to_json()); + Http::response(201, $user->to_json(), 'application/json'); $info = array('error' => sprintf(__('%s: Unknown or invalid ID.'), _N('end user', 'end users', 1))); @@ -297,7 +297,7 @@ class UsersAjaxAPI extends AjaxController { $info['title'] = __('Add New User'); $form = UserForm::getUserForm()->getForm($_POST); if (($user = User::fromForm($form))) - Http::response(201, $user->to_json()); + Http::response(201, $user->to_json(), 'application/json'); $info['error'] = sprintf('%s - %s', __('Error adding user'), __('Please try again!')); } @@ -433,7 +433,7 @@ class UsersAjaxAPI extends AjaxController { } if ($org && $user->setOrganization($org)) - Http::response(201, $org->to_json()); + Http::response(201, $org->to_json(), 'application/json'); elseif (! $info['error']) $info['error'] = __('Unable to add user to organization.') .' '.__('Correct any errors below and try again.'); diff --git a/include/class.client.php b/include/class.client.php index 11b500e4aef370bf60846024ada0cf8cf645df55..2d157ede502923d5e88fcbfa90f79924346c30b7 100644 --- a/include/class.client.php +++ b/include/class.client.php @@ -59,19 +59,20 @@ implements EmailContact, ITicketUser, TemplateVariable { case 'ticket_link': $qstr = array(); if ($cfg && $cfg->isAuthTokenEnabled() - && ($ticket=$this->getTicket()) - && !$ticket->getThread()->getNumCollaborators()) { - $qstr['auth'] = $ticket->getAuthToken($this); - return sprintf('%s/view.php?%s', - $cfg->getBaseUrl(), - Http::build_query($qstr, false) - ); - } - else { - return sprintf('%s/tickets.php?id=%s', - $cfg->getBaseUrl(), - $ticket->getId() - ); + && ($ticket=$this->getTicket())) { + if (!$ticket->getThread()->getNumCollaborators()) { + $qstr['auth'] = $ticket->getAuthToken($this); + return sprintf('%s/view.php?%s', + $cfg->getBaseUrl(), + Http::build_query($qstr, false) + ); + } + else { + return sprintf('%s/tickets.php?id=%s', + $cfg->getBaseUrl(), + $ticket->getId() + ); + } } diff --git a/include/class.config.php b/include/class.config.php index d7518a91f947786ba15ba69a345f5ee2018a7281..8cdf808e8838c6e19f1d73d1c97f0dc7094df76c 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -767,6 +767,10 @@ class OsticketConfig extends Config { return $sequence; } + function showTopLevelTicketCounts() { + return ($this->get('queue_bucket_counts')); + } + function getDefaultTicketNumberFormat() { return $this->get('ticket_number_format'); } @@ -1277,6 +1281,7 @@ class OsticketConfig extends Config { return $this->updateAll(array( 'ticket_number_format'=>$vars['ticket_number_format'] ?: '######', 'ticket_sequence_id'=>$vars['ticket_sequence_id'] ?: 0, + 'queue_bucket_counts'=>isset($vars['queue_bucket_counts'])?1:0, 'default_priority_id'=>$vars['default_priority_id'], 'default_help_topic'=>$vars['default_help_topic'], 'default_ticket_status_id'=>$vars['default_ticket_status_id'], diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php index 7d654ed053f2998185e27e89e1f055a11348d2fd..6b6212c6a278329cdec9ee089b083001ad7818a1 100644 --- a/include/class.dynamic_forms.php +++ b/include/class.dynamic_forms.php @@ -1098,16 +1098,18 @@ class DynamicFormEntry extends VerySimpleModel { return !$this->_errors; } - function isValidForClient() { - $filter = function($f) { - return $f->isVisibleToUsers(); + function isValidForClient($update=false) { + $filter = function($f) use($update) { + return $update ? $f->isEditableToUsers() : + $f->isVisibleToUsers(); }; return $this->isValid($filter); } - function isValidForStaff() { - $filter = function($f) { - return $f->isVisibleToStaff(); + function isValidForStaff($update=false) { + $filter = function($f) use($update) { + return $update ? $f->isEditableToStaff() : + $f->isVisibleToStaff(); }; return $this->isValid($filter); } @@ -1238,6 +1240,15 @@ class DynamicFormEntry extends VerySimpleModel { } } + /** + * Save the form entry and all associated answers. + * + */ + + function save($refetch=false) { + return $this->saveAnswers(null, $refetch); + } + /** * Save the form entry and all associated answers. * @@ -1245,7 +1256,8 @@ class DynamicFormEntry extends VerySimpleModel { * (mixed) FALSE if updated failed, otherwise the number of dirty answers * which were save is returned (which may be ZERO). */ - function save($refetch=false) { + + function saveAnswers($isEditable=null, $refetch=false) { if (count($this->dirty)) $this->set('updated', new SqlFunction('NOW')); @@ -1255,12 +1267,12 @@ class DynamicFormEntry extends VerySimpleModel { $dirty = 0; foreach ($this->getAnswers() as $a) { $field = $a->getField(); - // Don't save answers for presentation-only fields or fields - // which are stored elsewhere - if (!$field->hasData() || !$field->isStorable() - || $field->isPresentationOnly() - ) { + // which are stored elsewhere or those which are not editable + if (!$field->hasData() + || !$field->isStorable() + || $field->isPresentationOnly() + || ($isEditable && !$isEditable($field))) { continue; } // Set the entry here so that $field->getClean() can use the diff --git a/include/class.faq.php b/include/class.faq.php index 555fd3aee7f6f01fb485259e338c13c394dc826b..38e20b55cfa14b176eeae22913f96448e8b850d3 100644 --- a/include/class.faq.php +++ b/include/class.faq.php @@ -405,7 +405,7 @@ class FAQ extends VerySimpleModel { } $images = Draft::getAttachmentIds($vars['answer']); - $images = array_map(function($i) { return $i['id']; }, $images); + $images = array_flip(array_map(function($i) { return $i['id']; }, $images)); $this->getAttachments()->keepOnlyFileIds($images, true); // Handle language-specific attachments diff --git a/include/class.file.php b/include/class.file.php index 5bbd3d8859b1303648bc6464a9075040b8a1e3c9..419a2820922f6405f83d236b8cc47934f566dbc5 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -197,6 +197,18 @@ class AttachmentFile extends VerySimpleModel { $options); } + // Generates full download URL for external sources. + // e.g. https://domain.tld/file.php?args=123 + function getExternalDownloadUrl($options=array()) { + global $cfg; + + $download = self::getDownloadUrl($options); + // Separate URL handle and args + list($handle, $args) = explode('file.php?', $download); + + return (string) rtrim($cfg->getBaseUrl(), '/').'/file.php?'.$args; + } + static function generateDownloadUrl($id, $key, $hash, $options = array()) { // Expire at the nearest midnight, allow at least12 hrs access diff --git a/include/class.format.php b/include/class.format.php index d3d9d7ae9ae68e7994e302fc70be0d079f6cae35..216cb6e630ea42be0d8ca88bdca19a372c33b182 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -503,6 +503,22 @@ class Format { return implode( $separator, $string ); } + function number($number, $locale=false) { + if (is_array($number)) + return array_map(array('Format','number'), $number); + + if (!is_numeric($number)) + return $number; + + if (extension_loaded('intl') && class_exists('NumberFormatter')) { + $nf = NumberFormatter::create($locale ?: Internationalization::getCurrentLocale(), + NumberFormatter::DECIMAL); + return $nf->format($number); + } + + return number_format((int) $number); + } + /* elapsed time */ function elapsedTime($sec) { @@ -1001,11 +1017,11 @@ implements TemplateVariable { case 'short': return Format::date($this->date, $this->fromdb, false, $this->timezone, $this->user); case 'long': - return Format::datetime($this->date, $this->fromdb, $this->timezone, $this->user); + return Format::datetime($this->date, $this->fromdb, false, $this->timezone, $this->user); case 'time': return Format::time($this->date, $this->fromdb, false, $this->timezone, $this->user); case 'full': - return Format::daydatetime($this->date, $this->fromdb, $this->timezone, $this->user); + return Format::daydatetime($this->date, $this->fromdb, false, $this->timezone, $this->user); } } diff --git a/include/class.forms.php b/include/class.forms.php index 1b13c175f637adc8614e746aa10f522d4ade7d1c..d199d22a64e07a7cb97b562139d8aab2317286df 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -43,7 +43,7 @@ class Form { $this->id = $options['id']; // Use POST data if source was not specified - $this->_source = ($source) ? $source : $_POST; + $this->_source = $source ?: $_POST; } function getFormId() { @@ -89,6 +89,28 @@ class Form { return $this->getField($name); } + function hasAnyEnabledFields() { + return $this->hasAnyVisibleFields(false); + } + + function hasAnyVisibleFields($user=false) { + $visible = 0; + $isstaff = $user instanceof Staff; + foreach ($this->getFields() as $F) { + if (!$user) { + // Assume hasAnyEnabledFields + if ($F->isEnabled()) + $visible++; + } elseif($isstaff) { + if ($F->isVisibleToStaff()) + $visible++; + } elseif ($F->isVisibleToUsers()) { + $visible++; + } + } + return $visible > 0; + } + function getTitle() { return $this->title; } function getInstructions() { return $this->instructions; } function getSource() { return $this->_source; } @@ -120,7 +142,7 @@ class Form { } } - function getClean() { + function getClean($validate=true) { if (!$this->_clean) { $this->_clean = array(); foreach ($this->getFields() as $key=>$field) { @@ -131,7 +153,7 @@ class Form { if (is_int($key) && $field->get('id')) $key = $field->get('id'); $this->_clean[$key] = $this->_clean[$field->get('name')] - = $field->getClean(); + = $field->getClean($validate); } unset($this->_clean[""]); } @@ -606,7 +628,7 @@ class FormField { * submitted via POST, in order to kick off parsing and validation of * user-entered data. */ - function getClean() { + function getClean($validate=true) { if (!isset($this->_clean)) { $this->_clean = (isset($this->value)) // XXX: The widget value may be parsed already if this is @@ -628,7 +650,7 @@ class FormField { if (!isset($this->_clean) && ($d = $this->get('default'))) $this->_clean = $d; - if ($this->isVisible()) + if ($this->isVisible() && $validate) $this->validateEntry($this->_clean); } return $this->_clean; @@ -836,7 +858,7 @@ class FormField { * Returns an HTML friendly value for the data in the field. */ function display($value) { - return Format::htmlchars($this->toString($value)); + return Format::htmlchars($this->toString($value ?: $this->value)); } /** @@ -3438,7 +3460,7 @@ class FileUploadField extends FormField { $files[] = $f; } - foreach (@$this->getClean() as $key => $value) + foreach ($this->getClean(false) ?: array() as $key => $value) $files[] = array('id' => $key, 'name' => $value); $this->files = $files; @@ -4482,7 +4504,7 @@ class FileUploadWidget extends Widget { ); $maxfilesize = ($config['size'] ?: 1048576) / 1048576; $files = array(); - $new = $this->field->getClean(); + $new = $this->field->getClean(false); foreach ($this->field->getAttachments() as $att) { unset($new[$att->file_id]); @@ -4594,7 +4616,7 @@ class FileUploadWidget extends Widget { continue; // Keep the values as the IDs - $ids[$id] = $name; + $ids[$id] = $name ?: $allowed[$id]; } return $ids; @@ -5082,6 +5104,49 @@ class ReleaseForm extends Form { } } +class MarkAsForm extends Form { + static $id = 'markAs'; + + 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 marking ticket as (un)answered'), + ), + ) + ), + ); + + + $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.page.php b/include/class.page.php index a430fc2e66cd20da6b5a1711b1739094bbc8fdbc..1eb7387108f6ea98febbf1130cdc051cb8bf67f3 100644 --- a/include/class.page.php +++ b/include/class.page.php @@ -282,7 +282,7 @@ class Page extends VerySimpleModel { // Attach inline attachments from the editor $keepers = Draft::getAttachmentIds($vars['body']); - $keepers = array_map(function($i) { return $i['id']; }, $keepers); + $keepers = array_flip(array_map(function($i) { return $i['id']; }, $keepers)); $this->attachments->keepOnlyFileIds($keepers, true); if ($rv) diff --git a/include/class.queue.php b/include/class.queue.php index 819327074d1ed64219afa96794c47897ddad0d61..a7b84f47fe8a25589acf1b7b2bcb688e1abd40e6 100644 --- a/include/class.queue.php +++ b/include/class.queue.php @@ -2377,7 +2377,7 @@ extends VerySimpleModel { $reverse = $reverse ? '-' : ''; $query = $query->order_by("{$reverse}{$alias}"); - } else { + } elseif($keys[0]) { list($path, $field) = $keys[0]; $query = $field->applyOrderBy($query, $reverse, $path); } diff --git a/include/class.report.php b/include/class.report.php index dbff0ba38f68714692c091d9214b079223351360..418eb407ee18982ca939b7f6f1884fafb5ba6847 100644 --- a/include/class.report.php +++ b/include/class.report.php @@ -215,10 +215,12 @@ class OverviewReport { $pk = 'dept__id'; $stats = $stats ->filter(array('dept_id__in' => $thisstaff->getDepts())) - ->values('dept__id', 'dept__name', 'dept__flags'); + ->values('dept__id', 'dept__name', 'dept__flags') + ->distinct('dept__id'); $times = $times ->filter(array('dept_id__in' => $thisstaff->getDepts())) - ->values('dept__id'); + ->values('dept__id') + ->distinct('dept__id'); break; case 'topic': $headers = array(__('Help Topic')); @@ -227,10 +229,12 @@ class OverviewReport { $topics = Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED); $stats = $stats ->values('topic_id', 'topic__topic', 'topic__flags') - ->filter(array('dept_id__in' => $thisstaff->getDepts(), 'topic_id__gt' => 0, 'topic_id__in' => array_keys($topics))); + ->filter(array('dept_id__in' => $thisstaff->getDepts(), 'topic_id__gt' => 0, 'topic_id__in' => array_keys($topics))) + ->distinct('topic_id'); $times = $times ->values('topic_id') - ->filter(array('topic_id__gt' => 0)); + ->filter(array('topic_id__gt' => 0)) + ->distinct('topic_id'); break; case 'staff': $headers = array(__('Agent')); @@ -240,8 +244,9 @@ class OverviewReport { $staff = Staff::getStaffMembers(); $stats = $stats ->values('staff_id', 'staff__firstname', 'staff__lastname') - ->filter(array('staff_id__in' => array_keys($staff))); - $times = $times->values('staff_id'); + ->filter(array('staff_id__in' => array_keys($staff))) + ->distinct('staff_id'); + $times = $times->values('staff_id')->distinct('staff_id'); $depts = $thisstaff->getManagedDepartments(); if ($thisstaff->hasPerm(ReportModel::PERM_AGENTS)) $depts = array_merge($depts, $thisstaff->getDepts()); diff --git a/include/class.search.php b/include/class.search.php index 6fa8e18c93e51e8e1e9e119f92fdc5b458c6c20a..e701a3f097c901a02e7973d5baa415ed51381865 100755 --- a/include/class.search.php +++ b/include/class.search.php @@ -847,41 +847,45 @@ class SavedQueue extends CustomQueue { return (!$errors); } + function getTotal($agent=null) { + $query = $this->getQuery(); + if ($agent) + $query = $agent->applyVisibility($query); + $query->limit(false)->offset(false)->order_by(false); + try { + return $query->count(); + } catch (Exception $e) { + return null; + } + } + function getCount($agent, $cached=true) { - $criteria = $cached ? array() : array('id' => $this->getId()); - $counts = self::counts($agent, $criteria, $cached); - return $counts["q{$this->getId()}"] ?: 0; + $count = null; + if ($cached && ($counts = self::counts($agent, $cached))) + $count = $counts["q{$this->getId()}"]; + + if ($count == null) + $count = $this->getTotal($agent); + + return $count; } // Get ticket counts for queues the agent has acces to. - static function counts($agent, $criteria=array(), $cached=true) { + static function counts($agent, $cached=true, $criteria=array()) { if (!$agent instanceof Staff) - return array(); + return null; // Cache TLS in seconds - $ttl = 3600; + $ttl = 5*60; // Cache key based on agent and salt of the installation $key = "counts.queues.{$agent->getId()}.".SECRET_SALT; if ($criteria && is_array($criteria)) // Consider additional criteria. $key .= '.'.md5(serialize($criteria)); // only consider cache if requesed - if ($cached) { - if (function_exists('apcu_store')) { - $found = false; - $counts = apcu_fetch($key, $found); - if ($found === true) - return $counts; - } elseif (isset($_SESSION[$key]) - && isset($_SESSION[$key]['qcount']) - && (time() - $_SESSION[$key]['time']) < $ttl) { - return $_SESSION[$key]['qcount']; - } else { - // Auto clear missed session cache (if any) - unset($_SESSION[$key]); - } - } + if ($cached && ($counts=self::getCounts($key, $ttl))) + return $counts; $queues = static::objects() ->filter(Q::any(array( @@ -913,17 +917,45 @@ class SavedQueue extends CustomQueue { } } - $counts = $query->values()->one(); + try { + $counts = $query->values()->one(); + } catch (Exception $ex) { + foreach ($queues as $q) + $counts['q'.$q->getId()] = $q->getTotal(); + } + // Always cache the results + self::storeCounts($key, $counts, $ttl); + + return $counts; + } + + static function getCounts($key, $ttl) { + + if (!$key) { + return array(); + } elseif (function_exists('apcu_store')) { + $found = false; + $counts = apcu_fetch($key, $found); + if ($found === true) + return $counts; + } elseif (isset($_SESSION['qcounts'][$key]) + && (time() - $_SESSION['qcounts'][$key]['time']) < $ttl) { + return $_SESSION['qcounts'][$key]['counts']; + } else { + // Auto clear missed session cache (if any) + unset($_SESSION['qcounts'][$key]); + } + } + + static function storeCounts($key, $counts, $ttl) { if (function_exists('apcu_store')) { apcu_store($key, $counts, $ttl); } else { // Poor man's cache - $_SESSION[$key]['qcount'] = $counts; - $_SESSION[$key]['time'] = time(); + $_SESSION['qcounts'][$key]['counts'] = $counts; + $_SESSION['qcounts'][$key]['time'] = time(); } - - return $counts; } static function clearCounts() { @@ -1533,6 +1565,11 @@ class TicketStatusChoiceField extends SelectionField { return parent::getSearchQ($method, $value, $name); } } + + function applyOrderBy($query, $reverse=false, $name=false) { + $reverse = $reverse ? '-' : ''; + return $query->order_by("{$reverse}status__name"); + } } class TicketThreadCountField extends NumericField { diff --git a/include/class.thread.php b/include/class.thread.php index 9488835a3d80c68f1a8a949f2d21094f4b414368..343b3e9005689e01a11ef5611e3a00179b776f82 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -2964,6 +2964,14 @@ implements TemplateVariable { return $resp; } + function __toString() { + return $this->asVar(); + } + + function asVar() { + return $this->getVar('complete'); + } + function getVar($name) { switch ($name) { case 'original': @@ -2983,12 +2991,23 @@ implements TemplateVariable { if ($entry) return $entry->getBody(); + break; + case 'complete': + $content = ''; + $thread = $this; + ob_start(); + include INCLUDE_DIR.'client/templates/thread-export.tmpl.php'; + $content = ob_get_contents(); + ob_end_clean(); + return $content; + break; } } static function getVarScope() { return array( + 'complete' => __('Thread Correspondence'), 'original' => array('class' => 'MessageThreadEntry', 'desc' => __('Original Message')), 'lastmessage' => array('class' => 'MessageThreadEntry', 'desc' => __('Last Message')), ); diff --git a/include/class.ticket.php b/include/class.ticket.php index 37c80c73c669fa1b9f59dbbebfed6eb563b3fc3d..a3f33f0521c2bb510b7e6a24fd07145c6b14b577 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -3300,22 +3300,25 @@ implements RestrictedAccess, Threadable, Searchable { if (!$this->save()) return false; - $vars['note'] = ThreadEntryBody::clean($vars['note']); + $vars['note'] = ThreadEntryBody::clean($vars['note']); if ($vars['note']) $this->logNote(_S('Ticket Updated'), $vars['note'], $thisstaff); // Update dynamic meta-data - foreach ($forms as $f) { - if ($C = $f->getChanges()) + foreach ($forms as $form) { + if ($C = $form->getChanges()) $changes['fields'] = ($changes['fields'] ?: array()) + $C; // Drop deleted forms - $idx = array_search($f->getId(), $vars['forms']); + $idx = array_search($form->getId(), $vars['forms']); if ($idx === false) { - $f->delete(); + $form->delete(); } else { - $f->set('sort', $idx); - $f->save(); + $form->set('sort', $idx); + $form->saveAnswers(function($f) { + return $f->isVisibleToStaff() + && $f->isEditableToStaff(); } + ); } } @@ -3358,7 +3361,10 @@ implements RestrictedAccess, Threadable, Searchable { __($field->getLabel())); else { if ($field->answer) { - if (!$field->save()) + if (!$field->isEditableToStaff()) + $errors['field'] = sprintf(__('%s can not be edited'), + __($field->getLabel())); + elseif (!$field->save()) $errors['field'] = __('Unable to update field'); $changes['fields'] = array($field->getId() => $changes); } else { diff --git a/include/class.user.php b/include/class.user.php index 8d6bd1a7bb76f9197d1b6b1844271aec337121d2..36e1df54848c17b7e8de08268135820cb0079ad0 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -359,7 +359,7 @@ implements TemplateVariable, Searchable { 'email' => (string) $this->getEmail(), 'phone' => (string) $this->getPhoneNumber()); - return JsonDataEncoder::encode($info); + return Format::json_encode($info); } function __toString() { @@ -451,19 +451,20 @@ implements TemplateVariable, Searchable { return $vars; } - function getForms($data=null) { + function getForms($data=null, $cb=null) { if (!isset($this->_forms)) { $this->_forms = array(); + $cb = $cb ?: function ($f) use($data) { return ($data); }; foreach ($this->getDynamicData() as $entry) { $entry->addMissingFields(); - if(!$data - && ($form = $entry->getDynamicForm()) + if(($form = $entry->getDynamicForm()) && $form->get('type') == 'U' ) { + foreach ($entry->getFields() as $f) { - if ($f->get('name') == 'name') + if ($f->get('name') == 'name' && !$cb($f)) $f->value = $this->getFullName(); - elseif ($f->get('name') == 'email') + elseif ($f->get('name') == 'email' && !$cb($f)) $f->value = $this->getEmail(); } } @@ -529,17 +530,23 @@ implements TemplateVariable, Searchable { function updateInfo($vars, &$errors, $staff=false) { + + $isEditable = function ($f) use($staff) { + return ($staff ? $f->isEditableToStaff() : + $f->isEditableToUsers()); + }; $valid = true; - $forms = $this->getForms($vars); + $forms = $this->getForms($vars, $isEditable); foreach ($forms as $entry) { $entry->setSource($vars); - if ($staff && !$entry->isValidForStaff()) + if ($staff && !$entry->isValidForStaff(true)) $valid = false; - elseif (!$staff && !$entry->isValidForClient()) + elseif (!$staff && !$entry->isValidForClient(true)) $valid = false; elseif ($entry->getDynamicForm()->get('type') == 'U' && ($f=$entry->getField('email')) - && $f->getClean() + && $isEditable($f) + && $f->getClean() && ($u=User::lookup(array('emails__address'=>$f->getClean()))) && $u->id != $this->getId()) { $valid = false; @@ -558,7 +565,7 @@ implements TemplateVariable, Searchable { foreach ($forms as $entry) { if ($entry->getDynamicForm()->get('type') == 'U') { // Name field - if (($name = $entry->getField('name'))) { + if (($name = $entry->getField('name')) && $isEditable($name) ) { $name = $name->getClean(); if (is_array($name)) $name = implode(', ', $name); @@ -566,14 +573,15 @@ implements TemplateVariable, Searchable { } // Email address field - if (($email = $entry->getField('email'))) { + if (($email = $entry->getField('email')) + && $isEditable($email)) { $this->default_email->address = $email->getClean(); $this->default_email->save(); } } - // DynamicFormEntry::save returns the number of answers updated - if ($entry->save()) { + // DynamicFormEntry::saveAnswers returns the number of answers updated + if ($entry->saveAnswers($isEditable)) { $this->updated = SqlFunction::NOW(); } } diff --git a/include/client/open.inc.php b/include/client/open.inc.php index 4a4022c8df3cc8a98405e576942fd7a2d9599ba2..d0a96effc9dbfa94f840ea6ec5e14de4a7c7d881 100644 --- a/include/client/open.inc.php +++ b/include/client/open.inc.php @@ -42,7 +42,7 @@ if ($info['topicId'] && ($topic=Topic::lookup($info['topicId']))) { if (!$thisclient) { $uform = UserForm::getUserForm()->getForm($_POST); if ($_POST) $uform->isValid(); - $uform->render(array('staff' => false)); + $uform->render(false, false, array('mode' => 'create')); } else { ?> <tr><td colspan="2"><hr /></td></tr> @@ -89,7 +89,9 @@ if ($info['topicId'] && ($topic=Topic::lookup($info['topicId']))) { </tr> </tbody> <tbody id="dynamic-form"> - <?php foreach ($forms as $form) { + <?php + $options = array('mode' => 'create'); + foreach ($forms as $form) { include(CLIENTINC_DIR . 'templates/dynamic-form.tmpl.php'); } ?> </tbody> diff --git a/include/client/templates/dynamic-form.tmpl.php b/include/client/templates/dynamic-form.tmpl.php index cfde56a64e9d00c27adc7ed06290c51cb0b69346..f668f4320daf5e690a3095aed43aae96bcfb486e 100644 --- a/include/client/templates/dynamic-form.tmpl.php +++ b/include/client/templates/dynamic-form.tmpl.php @@ -1,8 +1,11 @@ <?php - // Form headline and deck with a horizontal divider above and an extra - // space below. - // XXX: Would be nice to handle the decoration with a CSS class - ?> +// Return if no visible fields +global $thisclient; +if (!$form->hasAnyVisibleFields($thisclient)) + return; + +$isCreate = (isset($options['mode']) && $options['mode'] == 'create'); +?> <tr><td colspan="2"><hr /> <div class="form-header" style="margin-bottom:0.5em"> <h3><?php echo Format::htmlchars($form->getTitle()); ?></h3> @@ -12,13 +15,19 @@ <?php // Form fields, each with corresponding errors follows. Fields marked // 'private' are not included in the output for clients - global $thisclient; foreach ($form->getFields() as $field) { - if (isset($options['mode']) && $options['mode'] == 'create') { - if (!$field->isVisibleToUsers() && !$field->isRequiredForUsers()) + try { + if (!$field->isEnabled()) continue; } - elseif (!$field->isVisibleToUsers() && !$field->isEditableToUsers()) { + catch (Exception $e) { + // Not connected to a DynamicFormField + } + + if ($isCreate) { + if (!$field->isVisibleToUsers() && !$field->isRequiredForUsers()) + continue; + } elseif (!$field->isVisibleToUsers()) { continue; } ?> @@ -28,7 +37,8 @@ <label for="<?php echo $field->getFormName(); ?>"><span class="<?php if ($field->isRequiredForUsers()) echo 'required'; ?>"> <?php echo Format::htmlchars($field->getLocal('label')); ?> - <?php if ($field->isRequiredForUsers()) { ?> + <?php if ($field->isRequiredForUsers() && + ($field->isEditableToUsers() || $isCreate)) { ?> <span class="error">*</span> <?php } ?></span><?php @@ -40,12 +50,22 @@ <br/> <?php } - $field->render(array('client'=>true)); - ?></label><?php - foreach ($field->errors() as $e) { ?> - <div class="error"><?php echo $e; ?></div> - <?php } - $field->renderExtras(array('client'=>true)); + if ($field->isEditableToUsers() || $isCreate) { + $field->render(array('client'=>true)); + ?></label><?php + foreach ($field->errors() as $e) { ?> + <div class="error"><?php echo $e; ?></div> + <?php } + $field->renderExtras(array('client'=>true)); + } else { + $val = ''; + if ($field->value) + $val = $field->display($field->value); + elseif (($a=$field->getAnswer())) + $val = $a->display(); + + echo sprintf('%s </label>', $val); + } ?> </td> </tr> diff --git a/include/client/templates/thread-export.tmpl.php b/include/client/templates/thread-export.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..5914a88da94a48a5da28c31dd1685a4e2f438ae4 --- /dev/null +++ b/include/client/templates/thread-export.tmpl.php @@ -0,0 +1,93 @@ +<?php +global $cfg; + +$entryTypes = array( + 'M' => array('color' => '#0088cc'), + 'R' => array('color' => '#e65524'), + ); + +AttachmentFile::objects()->filter(array( + 'attachments__thread_entry__thread__id' => $thread->getId(), + 'attachments__thread_entry__type__in' => array_keys($entryTypes) + ))->all(); + +$entries = $thread->getEntries(); +$entries->filter(array('type__in' => array_keys($entryTypes))); +?> +<style type="text/css"> + div {font-family: sans-serif;} +</style> +<div style="width: 100%; margin: 0; padding: 0;"> + <div style="padding:10px;"> + <p style="font-family: sans-serif; font-size:12px; color:#999;"> </p> + </div> + <table width="100%" cellpadding="0" cellspacing="0" border="0"> + <tbody> + <tr> + <td></td> + </tr> + <?php + foreach ($entries as $entry) { + $user = $entry->getUser() ?: $entry->getStaff(); + $name = $user ? $user->getName() : $entry->poster; + $color = $entryTypes[$entry->type]['color']; + ?> + <tr> + <td style=" border-top: 1px dashed #999;"> + <div style="background-color:#f7f7f7; padding:10px 20px;"> + <p style="font-family: sans-serif; padding:0; margin:0; color:<?php echo $color; ?>;"> + <strong><?php echo $name; ?></strong> + <span style="color:#888; font-size:12px; padding-left: 20px;"><?php + echo $entry->title; + ?> + </span> + </p> + <p style="font-family: sans-serif; padding:0; margin:0; color:#888; font-size:12px;"> + <?php + echo Format::daydatetime($entry->created); + ?> + </p> + </div> + <div style="padding:2px 20px;"> + <p style="font-family: sans-serif; font-size:14px; color:#555;"> + <?php + echo $entry->getBody()->display('email'); + ?> + </p> + <?php + if ($entry->has_attachments) { ?> + <p style="font-family: sans-serif; font-size:12px; line-height:20px; color:#888;"> + <?php echo __('Attachments'); ?> + <br /> + <?php + foreach ($entry->attachments as $a) { + if ($a->inline) continue; + $size = ''; + if ($a->file->size) + $size = sprintf('<small style="color:#ccc;"> (%s)</small>', + Format::file_size($a->file->size)); + + $filename = Format::htmlchars($a->getFilename()); + echo sprintf('<a href="%s" download="%s" + style="font-size:11px; color:#0088cc;" + target="_blank">%s</a> %s<br/>', + $a->file->getExternalDownloadUrl(), + $filename, + $filename, + $size); + } + ?> + </p> + <?php + } ?> + </div> + </td> + </tr> + <?php + } ?> + </tbody> + </table> + <div style="font-family: sans-serif; margin: 2px 0 14px 0; padding: 10px ; border-top: 1px solid #999; font-size:12px; color:#888;"> + + </div> +</div> diff --git a/include/i18n/en_US/config.yaml b/include/i18n/en_US/config.yaml index 731e51eb297b2e37469d40562c7cf23d4b28300e..e0c47f7149c49254782e2a020962e8ae4116c20a 100644 --- a/include/i18n/en_US/config.yaml +++ b/include/i18n/en_US/config.yaml @@ -77,6 +77,7 @@ core: email_attachments: 1 ticket_number_format: '######' ticket_sequence_id: 0 + queue_bucket_counts: 0 task_number_format: '#' task_sequence_id: 2 log_level: 2 diff --git a/include/i18n/en_US/help/tips/settings.ticket.yaml b/include/i18n/en_US/help/tips/settings.ticket.yaml index 3b0c1cea9005b7fea87da7a97a7d9702fda6a8af..a736db8e0cc29871f04d5936c296c605bde495d2 100644 --- a/include/i18n/en_US/help/tips/settings.ticket.yaml +++ b/include/i18n/en_US/help/tips/settings.ticket.yaml @@ -32,6 +32,12 @@ sequence_id: in the <span class="doc-desc-title">Ticket Number Format</span> configuration for help topics. +queue_bucket_counts: + title: Top-Level Ticket Counts + content: > + This setting is used to hide or show the ticket counts on Main-Level + queues. Get back to the way things used to be. + default_ticket_status: title: Default Status for new Tickets content: > diff --git a/include/i18n/en_US/queue.yaml b/include/i18n/en_US/queue.yaml index 34a35c28707db92ce80c80af9ed8728b1cd1ce7d..83870a34706954d96c8af7e8038f813e137a4b72 100644 --- a/include/i18n/en_US/queue.yaml +++ b/include/i18n/en_US/queue.yaml @@ -32,6 +32,7 @@ parent_id: 0 flags: 0x03 sort: 1 + sort_id: 1 root: T config: '[["status__state","includes",{"open":"Open"}]]' columns: @@ -204,6 +205,7 @@ flags: 0x03 root: T sort: 3 + sort_id: 3 config: '{"criteria":[["assignee","includes",{"M":"Me","T":"One of my teams"}],["status__state","includes",{"open":"Open"}]],"conditions":[]}' columns: - column_id: 1 diff --git a/include/i18n/en_US/queue_column.yaml b/include/i18n/en_US/queue_column.yaml index 03250da19a83b3118bbd04eb4792218c232928dd..87b3c09c13bee6d5b4757ca6329f35a715ff6f28 100644 --- a/include/i18n/en_US/queue_column.yaml +++ b/include/i18n/en_US/queue_column.yaml @@ -69,8 +69,8 @@ conditions: "[]" - id: 6 - name: "Status Name" - primary: "status__name" + name: "Status" + primary: "status__id" truncate: "wrap" annotations: "[]" conditions: "[]" diff --git a/include/staff/department.inc.php b/include/staff/department.inc.php index caab736fde8588f807d885fcaac6193819bd3451..b743aa5f62add15ab1e2e9cf42e937e04ccacdce 100644 --- a/include/staff/department.inc.php +++ b/include/staff/department.inc.php @@ -99,9 +99,9 @@ $info = Format::htmlchars(($errors && $_POST) ? $_POST : $info); </td> <td> <select name="status"> - <option value="active"<?php echo ($info['status'] == __('Active'))?'selected="selected"':'';?>><?php echo __('Active'); ?></option> - <option value="disabled"<?php echo ($info['status'] == __('Disabled'))?'selected="selected"':'';?>><?php echo __('Disabled'); ?></option> - <option value="archived"<?php echo ($info['status'] == __('Archived'))?'selected="selected"':'';?>><?php echo __('Archived'); ?></option> + <option value="active"<?php echo (!strcasecmp($info['status'], 'active'))?'selected="selected"':'';?>><?php echo __('Active'); ?></option> + <option value="disabled"<?php echo (!strcasecmp($info['status'], 'disabled'))?'selected="selected"':'';?>><?php echo __('Disabled'); ?></option> + <option value="archived"<?php echo (!strcasecmp($info['status'], 'archived'))?'selected="selected"':'';?>><?php echo __('Archived'); ?></option> </select> <span class="error"> </span> <i class="help-tip icon-question-sign" href="#status"></i> </td> diff --git a/include/staff/departments.inc.php b/include/staff/departments.inc.php index 4fd4a34c9fe0870e6c006886bd5a23d88e68d5ec..bec901732d9052c03ae0a660abc956ebdd12177d 100644 --- a/include/staff/departments.inc.php +++ b/include/staff/departments.inc.php @@ -143,7 +143,7 @@ $showing = $pageNav->showing().' '._N('department', 'departments', $count); echo Dept::getNameById($id); ?></a> <?php echo $default; ?> </td> <td><?php - if($dept->getStatus() == __('Active')) + if(!strcasecmp($dept->getStatus(), 'Active')) echo $dept->getStatus(); else echo '<b>'.$dept->getStatus(); diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php index 76db039c0ce14a36de83b1b9240e21e38f42f266..95636299e687419ee832990b63102ce511348cb3 100644 --- a/include/staff/settings-tickets.inc.php +++ b/include/staff/settings-tickets.inc.php @@ -65,6 +65,12 @@ if(!($maxfileuploads=ini_get('max_file_uploads'))) <i class="help-tip icon-question-sign" href="#sequence_id"></i> </td> </tr> + <tr><td width="220"><?php echo __('Top-Level Ticket Counts'); ?>:</td> + <td> + <input type="checkbox" name="queue_bucket_counts" <?php echo $config['queue_bucket_counts']?'checked="checked"':''; ?>> + <?php echo __('Enable'); ?> <i class="help-tip icon-question-sign" href="#queue_bucket_counts"></i> + </td> + </tr> <tr> <td width="180" class="required"> <?php echo __('Default Status'); ?>: diff --git a/include/staff/templates/dynamic-form.tmpl.php b/include/staff/templates/dynamic-form.tmpl.php index ef18c8bd01c5a988f063d80caea823da0f857ca1..00abf34cbb93d08e8d73c1e36ed6beed2e3ef61a 100644 --- a/include/staff/templates/dynamic-form.tmpl.php +++ b/include/staff/templates/dynamic-form.tmpl.php @@ -1,6 +1,8 @@ <?php -// If the form was removed using the trashcan option, and there was some -// other validation error, don't render the deleted form the second time +global $thisstaff; + +$isCreate = (isset($options['mode']) && $options['mode'] == 'create'); + if (isset($options['entry']) && $options['mode'] == 'edit' && $_POST && ($_POST['forms'] && !in_array($options['entry']->getId(), $_POST['forms'])) @@ -42,8 +44,6 @@ if (isset($options['entry']) && $options['mode'] == 'edit') { ?> try { if (!$field->isEnabled()) continue; - if ($options['mode'] == 'edit' && !$field->isEditableToStaff()) - continue; } catch (Exception $e) { // Not connected to a DynamicFormField @@ -60,41 +60,52 @@ if (isset($options['entry']) && $options['mode'] == 'edit') { ?> <?php echo Format::htmlchars($field->getLocal('label')); ?>:</td> <td><div style="position:relative"><?php } - $field->render($options); ?> - <?php if (!$field->isBlockLevel() && $field->isRequiredForStaff()) { ?> - <span class="error">*</span> - <?php - } - if ($field->isStorable() && ($a = $field->getAnswer()) && $a->isDeleted()) { - ?><a class="action-button float-right danger overlay" title="Delete this data" - href="#delete-answer" - onclick="javascript:if (confirm('<?php echo __('You sure?'); ?>')) - $.ajax({ - url: 'ajax.php/form/answer/' - +$(this).data('entryId') + '/' + $(this).data('fieldId'), - type: 'delete', - success: $.proxy(function() { - $(this).closest('tr').fadeOut(); - }, this) - });" - data-field-id="<?php echo $field->getAnswer()->get('field_id'); - ?>" data-entry-id="<?php echo $field->getAnswer()->get('entry_id'); - ?>"> <i class="icon-trash"></i> </a></div><?php - } - if ($a && !$a->getValue() && $field->isRequiredForClose()) { -?><i class="icon-warning-sign help-tip warning" - data-title="<?php echo __('Required to close ticket'); ?>" - data-content="<?php echo __('Data is required in this field in order to close the related ticket'); ?>" -/></i><?php - } - if ($field->get('hint') && !$field->isBlockLevel()) { ?> - <br /><em style="color:gray;display:inline-block"><?php - echo Format::viewableImages($field->getLocal('hint')); ?></em> - <?php - } - foreach ($field->errors() as $e) { ?> - <div class="error"><?php echo Format::htmlchars($e); ?></div> - <?php } ?> + + if ($field->isEditableToStaff() || $isCreate) { + $field->render($options); ?> + <?php if (!$field->isBlockLevel() && $field->isRequiredForStaff()) { ?> + <span class="error">*</span> + <?php + } + if ($field->isStorable() && ($a = $field->getAnswer()) && $a->isDeleted()) { + ?><a class="action-button float-right danger overlay" title="Delete this data" + href="#delete-answer" + onclick="javascript:if (confirm('<?php echo __('You sure?'); ?>')) + $.ajax({ + url: 'ajax.php/form/answer/' + +$(this).data('entryId') + '/' + $(this).data('fieldId'), + type: 'delete', + success: $.proxy(function() { + $(this).closest('tr').fadeOut(); + }, this) + });" + data-field-id="<?php echo $field->getAnswer()->get('field_id'); + ?>" data-entry-id="<?php echo $field->getAnswer()->get('entry_id'); + ?>"> <i class="icon-trash"></i> </a></div><?php + } + if ($a && !$a->getValue() && $field->isRequiredForClose()) { + ?><i class="icon-warning-sign help-tip warning" + data-title="<?php echo __('Required to close ticket'); ?>" + data-content="<?php echo __('Data is required in this field in order to close the related ticket'); ?>" + /></i><?php + } + if ($field->get('hint') && !$field->isBlockLevel()) { ?> + <br /><em style="color:gray;display:inline-block"><?php + echo Format::viewableImages($field->getLocal('hint')); ?></em> + <?php + } + foreach ($field->errors() as $e) { ?> + <div class="error"><?php echo Format::htmlchars($e); ?></div> + <?php } + } else { + $val = ''; + if ($field->value) + $val = $field->display($field->value); + elseif (($a= $field->getAnswer())) + $val = $a->display(); + + echo $val; + }?> </div></td> </tr> <?php } diff --git a/include/staff/templates/mark-as.tmpl.php b/include/staff/templates/mark-as.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..49c2d2c8664467dcf3ba6dfac34f575f8a471604 --- /dev/null +++ b/include/staff/templates/mark-as.tmpl.php @@ -0,0 +1,57 @@ +<?php +global $cfg; + +$form = MarkAsForm::instantiate($_POST); +?> +<h3 class="drag-handle"><?php echo $info[':title'] ?: __('Please Confirm'); ?></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(); ?>/mark/<?php echo $action; ?>" + name="markAs"> + <table width="100%"> + <tbody> + <tr><td> + <p> + <?php echo sprintf( + __('Are you sure you want to mark ticket as <b>%s</b>?'), + $action); ?> + </p> + <p> + <?php echo __('Please confirm to continue.'); ?> + </p> + </td></tr> + <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 __('OK'); ?>"> + </span> + </p> +</form> +<div class="clear"></div> diff --git a/include/staff/templates/queue-navigation.tmpl.php b/include/staff/templates/queue-navigation.tmpl.php index 380e03af961d5f8f6901d9bf63eadb55bb7679a4..f408c21843b1ce40b11cf9f70f4465d005b5c84d 100644 --- a/include/staff/templates/queue-navigation.tmpl.php +++ b/include/staff/templates/queue-navigation.tmpl.php @@ -4,6 +4,7 @@ // $q - <CustomQueue> object for this navigation entry // $selected - <bool> true if this queue is currently active // $child_selected - <bool> true if the selected queue is a descendent +global $cfg; $childs = $children; $this_queue = $q; $selected = (!isset($_REQUEST['a']) && $_REQUEST['queue'] == $this_queue->getId()); @@ -11,7 +12,15 @@ $selected = (!isset($_REQUEST['a']) && $_REQUEST['queue'] == $this_queue->getId <li class="top-queue item <?php if ($child_selected) echo 'child active'; elseif ($selected) echo 'active'; ?>"> <a href="<?php echo $this_queue->getHref(); ?>" - class="Ticket"><i class="small icon-sort-down pull-right"></i><?php echo $this_queue->getName(); ?></a> + class="Ticket"><i class="small icon-sort-down pull-right"></i><?php echo $this_queue->getName(); ?> +<?php if ($cfg->showTopLevelTicketCounts()) { ?> + <span id="queue-count-bucket" class="hidden"> + (<span class="queue-count" + data-queue-id="<?php echo $this_queue->id; ?>"><span class="faded-more"></span> + </span>) + </span> +<?php } ?> + </a> <div class="customQ-dropdown"> <ul class="scroll-height"> <!-- Add top-level queue (with count) --> diff --git a/include/staff/templates/queue-tickets.tmpl.php b/include/staff/templates/queue-tickets.tmpl.php index fe82ff9a21460eda9090a000f4fda147a99bc323..41ffef2a6303925094eece20861e9b420d2c308d 100644 --- a/include/staff/templates/queue-tickets.tmpl.php +++ b/include/staff/templates/queue-tickets.tmpl.php @@ -88,7 +88,7 @@ $tickets = $tickets->filter(['ticket_id__in' => $criteria->values_flat('ticket_i # Index hint should be used on the $criteria query only $tickets->clearOption(QuerySet::OPT_INDEX_HINT); -$count = $queue->getCount($thisstaff); +$count = $queue->getCount($thisstaff) ?: (PAGE_LIMIT*3); $pageNav->setTotal($count, true); $pageNav->setURL('tickets.php', $args); ?> diff --git a/include/staff/templates/user-lookup.tmpl.php b/include/staff/templates/user-lookup.tmpl.php index 62547c99bc7e1d8e59f6222d3859116b89437a97..f46fd7c99e9e4689183998212220730b6485973f 100644 --- a/include/staff/templates/user-lookup.tmpl.php +++ b/include/staff/templates/user-lookup.tmpl.php @@ -83,8 +83,8 @@ if ($user) { ?> <form method="post" class="user" action="<?php echo $info['action'] ?: '#users/lookup/form'; ?>"> <table width="100%" class="fixed"> <?php - if(!$form) $form = UserForm::getInstance(); - $form->render(['staff' => true, 'title' => __('Create New User')]); ?> + $form = $form ?: UserForm::getInstance(); + $form->render(true, __('Create New User'), array('mode' => 'create')); ?> </table> <hr> <p class="full-width"> diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php index 09bcfb21fdf95ee2a05366724b6ee921c63eaf48..267d927651833ba064e7bf38b1978b100433c752 100644 --- a/include/staff/ticket-open.inc.php +++ b/include/staff/ticket-open.inc.php @@ -348,6 +348,7 @@ if ($_POST) </tbody> <tbody id="dynamic-form"> <?php + $options = array('mode' => 'create'); foreach ($forms as $form) { print $form->getForm($_SESSION[':form-data'])->getMedia(); include(STAFFINC_DIR . 'templates/dynamic-form.tmpl.php'); diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 9fd5835b562c73b6ec5ce5e7e9f8b42b5cfc93f5..bc1573f354b5358d458d6962c8bca8b27d583680 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -22,6 +22,7 @@ $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 +$canAnswer = ($isManager || $role->hasPerm(Ticket::PERM_REPLY)); //Check if Agent can mark as answered/unanswered //Useful warnings and errors the user might want to know! if ($ticket->isClosed() && !$ticket->isReopenable()) @@ -161,13 +162,20 @@ if($ticket->isOverdue()) echo __('Mark as Overdue'); ?></a></li> <?php } + } elseif($ticket->isOpen() && $canAnswer) { if($ticket->isAnswered()) { ?> - <li><a class="confirm-action" id="ticket-unanswered" href="#unanswered"><i class="icon-circle-arrow-left"></i> <?php + <li><a href="#tickets/<?php echo $ticket->getId(); + ?>/mark/unanswered" class="ticket-action" + data-redirect="tickets.php?id=<?php echo $ticket->getId(); ?>"> + <i class="icon-circle-arrow-left"></i> <?php echo __('Mark as Unanswered'); ?></a></li> <?php } else { ?> - <li><a class="confirm-action" id="ticket-answered" href="#answered"><i class="icon-circle-arrow-right"></i> <?php + <li><a href="#tickets/<?php echo $ticket->getId(); + ?>/mark/answered" class="ticket-action" + data-redirect="tickets.php?id=<?php echo $ticket->getId(); ?>"> + <i class="icon-circle-arrow-right"></i> <?php echo __('Mark as Answered'); ?></a></li> <?php } @@ -598,7 +606,9 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) { ))); $displayed = array(); foreach($answers as $a) { - $displayed[] = array($a->getLocal('label'), $a->display() ?: '<span class="faded">—' . __('Empty') . '— </span>', $a->getLocal('id'), ($a->getField() instanceof FileUploadField)); + if (!$a->getField()->isVisibleToStaff()) + continue; + $displayed[] = $a; } if (count($displayed) == 0) continue; @@ -609,13 +619,18 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) { </thead> <tbody> <?php - foreach ($displayed as $stuff) { - list($label, $v, $id, $isFile) = $stuff; + foreach ($displayed as $a) { + $id = $a->getLocal('id'); + $label = $a->getLocal('label'); + $v = $a->display() ?: '<span class="faded">—' . __('Empty') . '— </span>'; + $field = $a->getField(); + $isFile = ($field instanceof FileUploadField); ?> <tr> <td width="200"><?php echo Format::htmlchars($label); ?>:</td> <td> - <?php if ($role->hasPerm(Ticket::PERM_EDIT)) { + <?php if ($role->hasPerm(Ticket::PERM_EDIT) + && $field->isEditableToStaff()) { $isEmpty = strpos($v, '—'); if ($isFile && !$isEmpty) echo $v.'<br>'; ?> @@ -633,7 +648,8 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) { echo $v; ?> </a> - <?php } else { + <?php + } else { echo $v; } ?> </td> diff --git a/include/upgrader/done.inc.php b/include/upgrader/done.inc.php index 39c598d7f49a283ca0539f11f024ad57812eefd9..ff3bb9ffd1660205d2620cda33d4466aa7f7a68a 100644 --- a/include/upgrader/done.inc.php +++ b/include/upgrader/done.inc.php @@ -9,12 +9,17 @@ $_SESSION['ost_upgrader']=null; <div id="intro"> <p><?php echo __('Congratulations! osTicket upgrade has been completed successfully.');?></p> <p><?php echo sprintf(__('Please refer to %s for more information about changes and/or new features.'), - sprintf('<a href="https://docs.osticket.com/en/latest/Developer%20Documentation/Changelog.html?highlight=notes" target="_blank">%s</a>', - __('Release Notes') + sprintf('<a href="%s" target="_blank">%s</a>', + 'https://docs.osticket.com/en/latest/Developer%20Documentation/Changelog.html?highlight=notes', + __('Release Notes') ));?></p> </div> <p><?php echo __('Once again, thank you for choosing osTicket.');?></p> - <p><?php echo sprintf(__('Please feel free to %1$s let us know %2$s of any other improvements and features you would like to see in osTicket, so that we may add them in the future as we continue to develop better and better versions of osTicket.'), '<a target="_blank" href="http://osticket.com/support/">', '</a>');?></p> + <p><?php echo sprintf(__('Please feel free to %1$s let us know %2$s + of any other improvements and features you would like to + see in osTicket, so that we may add them in the future + as we continue to develop better and better versions of + osTicket.'), '<a target="_blank" href="https://osticket.com/support/">', '</a>');?></p> <p><?php echo __("We take user feedback seriously and we're dedicated to making changes based on your input.");?></p> <p><?php echo __('Good luck.');?><p> <p><?php echo __('osTicket Team.');?></p> @@ -26,10 +31,15 @@ $_SESSION['ost_upgrader']=null; <p><b><?php echo __('Post-upgrade');?></b>: <?php echo sprintf(__('You can now go to %s to enable the system and explore the new features. For complete and up-to-date release notes see the %s'), sprintf('<a href="'. ROOT_PATH . 'scp/settings.php" target="_blank">%s</a>', __('Admin Panel')), - sprintf('<a href="https://docs.osticket.com/en/latest/Developer%20Documentation/Changelog.html?highlight=notes" target="_blank">%s</a>', __('osTicket Docs')));?></p> + sprintf('<a href="%s" target="_blank">%s</a>', + 'https://docs.osticket.com/en/latest/Developer%20Documentation/Changelog.html?highlight=notes', + __('osTicket Docs')));?></p> <p><b><?php echo __('Stay up to date');?></b>: <?php echo __("It's important to keep your osTicket installation up to date. Get announcements, security updates and alerts delivered directly to you!");?> - <?php echo sprintf(__('%1$s Get in the loop %2$s today and stay informed!'), '<a target="_blank" href="http://osticket.com/newsletter">', '</a>');?></p> - <p><b><?php echo __('Commercial Support Available');?></b>: <?php echo sprintf(__('Get guidance and hands-on expertise to address unique challenges and make sure your osTicket runs smoothly, efficiently, and securely. %1$s Learn More! %2$s'), '<a target="_blank" href="http://osticket.com/support">','</a>');?></p> + <?php echo sprintf(__('%1$s Get in the loop %2$s today and stay + informed!'), '<a target="_blank" href="https://osticket.com/">', '</a>');?></p> + <p><b><?php echo __('Commercial Support Available');?></b>: + <?php echo sprintf(__('Get guidance and hands-on expertise to address unique challenges and make sure your osTicket runs smoothly, efficiently, and securely. %1$s Learn More! %2$s'), + '<a target="_blank" href="https://osticket.com/">','</a>');?></p> </div> <div class="clear"></div> </div> diff --git a/js/osticket.js b/js/osticket.js index 4e0d512de80aaa87a35deb5a0165b4da86ce3b82..9584b92f8352666a4a57b2247c0a33221fad4b25 100644 --- a/js/osticket.js +++ b/js/osticket.js @@ -47,7 +47,15 @@ $(document).ready(function(){ $(window).unbind('beforeunload'); // Disable client-side Post Reply/Create Ticket buttons to help // prevent duplicate POST - $(':submit', $(this)).attr('disabled', true); + var form = $(this); + $(this).find('input[type="submit"]').each(function (index) { + // Clone original input + $(this).clone(false).removeAttr('id').prop('disabled', true).insertBefore($(this)); + + // Hide original input and add it to top of form + $(this).hide(); + form.prepend($(this)); + }); $('#overlay, #loading').show(); return true; }); diff --git a/scp/ajax.php b/scp/ajax.php index dfdb7499e7ae093c1a55e20f8937772d6447002e..304fab05b0956ca0c0169d502a3c8d44faa90b6d 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -168,6 +168,7 @@ $dispatcher = patterns('', 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+)/mark/(?P<action>\w+)$', 'markAs'), 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/autocron.php b/scp/autocron.php index cc124455f3a246ef97f37ce3fe7482d8d9797e93..c19e1520f4a465512ca3cd30e28d3fc26d49aec3 100644 --- a/scp/autocron.php +++ b/scp/autocron.php @@ -48,7 +48,7 @@ require_once(INCLUDE_DIR.'class.cron.php'); // Run tickets count every 3rd run or so... force new count by skipping cached // results if ((mt_rand(1, 12) % 3) == 0) - SavedQueue::counts($thisstaff, array(), false); + SavedQueue::counts($thisstaff, false); // Clear staff obj to avoid false credit internal notes & auto-assignment $thisstaff = null; diff --git a/scp/departments.php b/scp/departments.php index cb07697370fdefc681839ed7a59f81da10ab2daa..a4ca7252ec8c91edf44574daee009d0433567c5c 100644 --- a/scp/departments.php +++ b/scp/departments.php @@ -25,9 +25,6 @@ if($_REQUEST['id'] && !($dept=Dept::lookup($_REQUEST['id']))) if(!$dept){ $errors['err']=sprintf(__('%s: Unknown or invalid'), __('department')); }elseif($dept->update($_POST,$errors)){ - if ($_POST["status"] != __('Active')) - Dept::clearInactiveDept($dept->getId()); - $msg=sprintf(__('Successfully updated %s.'), __('this department')); }elseif(!$errors['err']){ @@ -133,8 +130,6 @@ if($_REQUEST['id'] && !($dept=Dept::lookup($_REQUEST['id']))) FilterAction::setFilterFlag($filter_actions, 'dept', true); if($d->save()) { $num++; - //set dept_id to default for topics/emails using disabled dept - Dept::clearInactiveDept($d->getId()); } } if ($num > 0) { diff --git a/scp/js/scp.js b/scp/js/scp.js index 2b346edcb469f9df5789ad3b591e6935b79bf473..025db554c60208d11e7754b1fe758a86f68478c7 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -166,7 +166,15 @@ var scp_prep = function() { $.toggleOverlay(true); // Disable staff-side Post Reply/Open buttons to help prevent // duplicate POST - $(':submit', $(this)).attr('disabled', true); + var form = $(this); + $(this).find('input[type="submit"]').each(function (index) { + // Clone original input + $(this).clone(false).removeAttr('id').prop('disabled', true).insertBefore($(this)); + + // Hide original input and add it to top of form + $(this).hide(); + form.prepend($(this)); + }); $('#overlay, #loading').show(); return true; }); @@ -526,9 +534,10 @@ var scp_prep = function() { url: 'ajax.php/queue/counts', dataType: 'json', success: function(json) { - $('li > span.queue-count').each(function(i, e) { + $('li span.queue-count').each(function(i, e) { var $e = $(e); $e.text(json['q' + $e.data('queueId')]); + $(e).parents().find('#queue-count-bucket').show(); }); } }); @@ -826,8 +835,9 @@ $.confirm = function(message, title, options) { }; $.userLookup = function (url, cb) { - $.dialog(url, 201, function (xhr) { - var user = $.parseJSON(xhr.responseText); + $.dialog(url, 201, function (xhr, user) { + if ($.type(user) == 'string') + user = $.parseJSON(user); if (cb) return cb(user); }, { onshow: function() { $('#user-search').focus(); } @@ -835,8 +845,9 @@ $.userLookup = function (url, cb) { }; $.orgLookup = function (url, cb) { - $.dialog(url, 201, function (xhr) { - var org = $.parseJSON(xhr.responseText); + $.dialog(url, 201, function (xhr, org) { + if ($.type(org) == 'string') + org = $.parseJSON(user); if (cb) cb(org); }, { onshow: function() { $('#org-search').focus(); } diff --git a/scp/queues.php b/scp/queues.php index 997fefa2678cae361a9654c6efecfaccf0199fe9..37fc13e3e2808fd252118ee10d20a12e0e983120 100644 --- a/scp/queues.php +++ b/scp/queues.php @@ -72,11 +72,13 @@ if ($_POST) { if ($queue->save()) $updated++; break; case 'delete': - if ($queue->delete()) $updated++; + if ($queue->getId() == $cfg->getDefaultTicketQueueId()) + $err = __('This queue is the default queue. Unable to delete. '); + elseif ($queue->delete()) $updated++; } } if (!$updated) { - Messages::error(__( + Messages::error($err ?: __( 'Unable to manage any of the selected queues')); } elseif ($_POST['count'] && $updated != $_POST['count']) { diff --git a/scp/tickets.php b/scp/tickets.php index 86fec81f28395a49905bcd3a342bfaf159be5d15..8881874e897217bbea32e02ba8735e482ee937ab 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -23,6 +23,11 @@ require_once(INCLUDE_DIR.'class.json.php'); require_once(INCLUDE_DIR.'class.dynamic_forms.php'); require_once(INCLUDE_DIR.'class.export.php'); // For paper sizes + + +// Fetch ticket queues organized by root and sub-queues +$queues = CustomQueue::getHierarchicalQueues($thisstaff); + $page=''; $ticket = $user = null; //clean start. $redirect = false; @@ -113,18 +118,21 @@ if (!$ticket) { $queue = AdhocSearch::load($key); } - // Make the current queue sticky - $_SESSION[$queue_key] = $queue_id; - - if ((int) $queue_id && !$queue) { + if ((int) $queue_id && !$queue) $queue = SavedQueue::lookup($queue_id); - } - if (!$queue) { - $queue = SavedQueue::lookup($cfg->getDefaultTicketQueueId()); - } - // Set the queue_id for navigation to turn a top-level item bold - $_REQUEST['queue'] = $queue->getId(); + if (!$queue && ($qid=$cfg->getDefaultTicketQueueId())) + $queue = SavedQueue::lookup($qid); + + if (!$queue && $queues) + list($queue,) = $queues[0]; + + if ($queue) { + // Set the queue_id for navigation to turn a top-level item bold + $_REQUEST['queue'] = $queue->getId(); + // Make the current queue sticky + $_SESSION[$queue_key] = $queue->getId(); + } } // Configure form for file uploads @@ -147,6 +155,8 @@ if($_POST && !$errors): $errors=array(); $lock = $ticket->getLock(); //Ticket lock if any $role = $ticket->getRole($thisstaff); + $dept = $ticket->getDept(); + $isManager = $dept->isManager($thisstaff); //Check if Agent is Manager switch(strtolower($_POST['a'])): case 'reply': if (!$role || !$role->hasPerm(Ticket::PERM_REPLY)) { @@ -302,8 +312,7 @@ if($_POST && !$errors): } break; case 'overdue': - $dept = $ticket->getDept(); - if(!$dept || !$dept->isManager($thisstaff)) { + if(!$dept || !$isManager) { $errors['err']=__('Permission Denied. You are not allowed to flag tickets overdue'); } elseif($ticket->markOverdue()) { $msg=sprintf(__('Ticket flagged as overdue by %s'),$thisstaff->getName()); @@ -312,28 +321,6 @@ if($_POST && !$errors): $errors['err']=sprintf('%s %s', __('Problems marking the the ticket overdue.'), __('Please try again!')); } break; - case 'answered': - $dept = $ticket->getDept(); - if(!$dept || !$dept->isManager($thisstaff)) { - $errors['err']=__('Permission Denied. You are not allowed to flag tickets'); - } elseif($ticket->markAnswered()) { - $msg=sprintf(__('Ticket flagged as answered by %s'),$thisstaff->getName()); - $ticket->logActivity(__('Ticket Marked Answered'),$msg); - } else { - $errors['err']=sprintf('%s %s', __('Problems marking the ticket answered.'), __('Please try again!')); - } - break; - case 'unanswered': - $dept = $ticket->getDept(); - if(!$dept || !$dept->isManager($thisstaff)) { - $errors['err']=__('Permission Denied. You are not allowed to flag tickets'); - } elseif($ticket->markUnAnswered()) { - $msg=sprintf(__('Ticket flagged as unanswered by %s'),$thisstaff->getName()); - $ticket->logActivity(__('Ticket Marked Unanswered'),$msg); - } else { - $errors['err']=sprintf('%s %s', __('Problems marking the ticket unanswered.'), __('Please try again!')); - } - break; case 'banemail': if (!$thisstaff->hasPerm(Email::PERM_BANLIST)) { $errors['err']=__('Permission Denied. You are not allowed to ban emails'); @@ -450,9 +437,6 @@ if (isset($_GET['clear_filter'])) $nav->setTabActive('tickets'); $nav->addSubNavInfo('jb-overflowmenu', 'customQ_nav'); -// Fetch ticket queues organized by root and sub-queues -$queues = CustomQueue::getHierarchicalQueues($thisstaff); - // Start with all the top-level (container) queues foreach ($queues as $_) { list($q, $children) = $_; diff --git a/tickets.php b/tickets.php index 560b16f3dc963008e05aa8c610607b82992fee95..93c7da3964d2460a8aadc2045b158c82cc959190 100644 --- a/tickets.php +++ b/tickets.php @@ -53,14 +53,17 @@ if ($_POST && is_object($ticket) && $ticket->getId()) { foreach ($forms as $form) { $form->filterFields(function($f) { return !$f->isStorable(); }); $form->setSource($_POST); - if (!$form->isValid()) + if (!$form->isValidForClient(true)) $errors = array_merge($errors, $form->errors()); } } if (!$errors) { - foreach ($forms as $f) { - $changes += $f->getChanges(); - $f->save(); + foreach ($forms as $form) { + $changes += $form->getChanges(); + $form->saveAnswers(function ($f) { + return $f->isVisibleToUsers() + && $f->isEditableToUsers(); }); + } if ($changes) { $user = User::lookup($thisclient->getId()); @@ -127,9 +130,9 @@ if($ticket && $ticket->checkUserAccess($thisclient)) { $inc = 'edit.inc.php'; if (!$forms) $forms=DynamicFormEntry::forTicket($ticket->getId()); // Auto add new fields to the entries - foreach ($forms as $f) { - $f->filterFields(function($f) { return !$f->isStorable(); }); - $f->addMissingFields(); + foreach ($forms as $form) { + $form->filterFields(function($f) { return !$f->isStorable(); }); + $form->addMissingFields(); } } else