diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 990ae735677c635ffbc7f1b78454e33df5282b82..9e416bef6a3961dcc549ff8381ed6bc93c2003ce 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -33,29 +33,29 @@ class TicketsAjaxAPI extends AjaxController { $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25; $tickets=array(); - $sql='SELECT DISTINCT `number`, email.address AS email' - .' FROM '.TICKET_TABLE.' ticket' - .' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id' - .' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id' - .' WHERE `number` LIKE \''.db_input($_REQUEST['q'], false).'%\''; - - $sql.=' AND ( staff_id='.db_input($thisstaff->getId()); - - if(($teams=$thisstaff->getTeams()) && count(array_filter($teams))) - $sql.=' OR team_id IN('.implode(',', db_input(array_filter($teams))).')'; - - if(!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts())) - $sql.=' OR dept_id IN ('.implode(',', db_input($depts)).')'; + $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)); + } - $sql.=' ) ' - .' ORDER BY ticket.created LIMIT '.$limit; - if(($res=db_query($sql)) && db_num_rows($res)) { - while(list($id, $email)=db_fetch_row($res)) { - $info = "$id - $email"; - $tickets[] = array('id'=>$id, 'email'=>$email, 'value'=>$id, - 'info'=>$info, 'matches'=>$_REQUEST['q']); - } + $hits = TicketModel::objects() + ->filter(Q::any(array( + 'number__startswith' => $_REQUEST['q'], + ))) + ->filter($visibility) + ->values('number', 'user__emails__address') + ->annotate(array('tickets' => SqlAggregate::COUNT('ticket_id'))) + ->order_by('-created') + ->limit($limit); + + foreach ($hits as $T) { + $tickets[] = array('id'=>$T['number'], 'value'=>$T['number'], + 'info'=>"{$T['number']} — {$T['user__emails__address']}", + 'matches'=>$_REQUEST['q']); } if (!$tickets) return self::lookupByEmail(); @@ -70,29 +70,31 @@ class TicketsAjaxAPI extends AjaxController { $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25; $tickets=array(); - $sql='SELECT email.address AS email, count(ticket.ticket_id) as tickets ' - .' FROM '.TICKET_TABLE.' ticket' - .' JOIN '.USER_TABLE.' user ON user.id = ticket.user_id' - .' JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id' - .' WHERE (email.address LIKE \'%'.db_input(strtolower($_REQUEST['q']), false).'%\' - OR user.name LIKE \'%'.db_input($_REQUEST['q'], false).'%\')'; - - $sql.=' AND ( staff_id='.db_input($thisstaff->getId()); - - if(($teams=$thisstaff->getTeams()) && count(array_filter($teams))) - $sql.=' OR team_id IN('.implode(',', db_input(array_filter($teams))).')'; - - if(!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts())) - $sql.=' OR dept_id IN ('.implode(',', db_input($depts)).')'; - - $sql.=' ) ' - .' GROUP BY email.address ' - .' ORDER BY ticket.created LIMIT '.$limit; + $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)); + } - if(($res=db_query($sql)) && db_num_rows($res)) { - while(list($email, $count)=db_fetch_row($res)) - $tickets[] = array('email'=>$email, 'value'=>$email, - 'info'=>"$email ($count)", 'matches'=>$_REQUEST['q']); + $hits = TicketModel::objects() + ->filter(Q::any(array( + 'user__emails__address__contains' => $_REQUEST['q'], + 'user__name__contains' => $_REQUEST['q'], + 'user__account__username' => $_REQUEST['q'], + 'user__org__name__contains' => $_REQUEST['q'], + ))) + ->filter($visibility) + ->values('user__emails__address') + ->annotate(array('tickets' => SqlAggregate::COUNT('ticket_id'))) + ->limit($limit); + + foreach ($hits as $T) { + $email = $T['user__emails__address']; + $count = $T['tickets']; + $tickets[] = array('email'=>$email, 'value'=>$email, + 'info'=>"$email ($count)", 'matches'=>$_REQUEST['q']); } return $this->json_encode($tickets); diff --git a/include/class.export.php b/include/class.export.php index 1e92c97f6d55522c2384f1837412cae44cbb07ec..288cb68480c0d850f57e91cb81938eae9fc4aa88 100644 --- a/include/class.export.php +++ b/include/class.export.php @@ -57,16 +57,19 @@ class Export { $cdata[$key] = $f->getLocal('label'); } // Reset the $sql query - $tickets = TicketModel::objects() - ->filter($sql->constraints) + $tickets = $sql->models() ->select_related('user', 'user__default_email', 'dept', 'staff', - 'team', 'staff', 'cdata'); - call_user_func_array(array($tickets, 'order_by'), $sql->getSortFields()); + 'team', 'staff', '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($tickets, array( 'number' => __('Ticket Number'), - 'created' => __('Date'), + 'created' => __('Date Created'), 'cdata.subject' => __('Subject'), 'user.name' => __('From'), 'user.default_email.address' => __('From Email'), @@ -75,14 +78,14 @@ class Export { 'topic::getName' => __('Help Topic'), 'source' => __('Source'), 'status::getName' =>__('Current Status'), - '::getEffectiveDate' => __('Last Updated'), - 'duedate' => __('Due Date'), + 'lastupdate' => __('Last Updated'), + 'est_duedate' => __('Due Date'), 'isoverdue' => __('Overdue'), 'isanswered' => __('Answered'), 'staff::getName' => __('Agent Assigned'), 'team::getName' => __('Team Assigned'), - #'thread_count' => __('Thread Count'), - #'attachments' => __('Attachment Count'), + 'thread_count' => __('Thread Count'), + 'attachment_count' => __('Attachment Count'), ) + $cdata, $how, array('modify' => function(&$record, $keys) use ($fields) { @@ -254,7 +257,7 @@ class ResultSetExporter { } } // Evalutate :: function call on target current - if ($func && method_exists($current, $func)) { + if ($func && (method_exists($current, $func) || method_exists($current, '__call'))) { $current = $current->{$func}(); } $record[] = (string) $current; diff --git a/include/class.format.php b/include/class.format.php index 02402cf70c45f7739d103fa7d462f7e4bede45b0..7b00d4adb277432b0c1a0801dbba691beb9a2696 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -444,10 +444,6 @@ class Format { if ($timestamp && $fromDb) { $timestamp = Misc::db2gmtime($timestamp); } - elseif (!$timestamp) { - $D = new DateTime(); - $timestamp = $D->getTimestamp(); - } if (class_exists('IntlDateFormatter')) { $formatter = new IntlDateFormatter( Internationalization::getCurrentLocale(), diff --git a/include/class.orm.php b/include/class.orm.php index c004ab455bd3c43252cdcb42947b78aaf2c1befe..ef615b39f63757fdd902ffe0627e2faf8ba9cf69 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -728,8 +728,9 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl return $this; } - function order_by() { - $this->ordering = array_merge($this->ordering, func_get_args()); + function order_by($order) { + $this->ordering = array_merge($this->ordering, + is_array($order) ? $order : func_get_args()); return $this; } function getSortFields() { @@ -754,6 +755,10 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl return $this; } + function isWindowed() { + return $this->limit || $this->offset; + } + function select_related() { $this->related = array_merge($this->related, func_get_args()); return $this; @@ -772,6 +777,12 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl return $this; } + function models() { + $this->iterator = 'ModelInstanceManager'; + $this->values = $this->related = array(); + return $this; + } + function values() { foreach (func_get_args() as $A) $this->values[$A] = $A; @@ -945,6 +956,30 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl return $this->query; } + /** + * Fetch a model class which can be used to render the QuerySet as a + * subquery to be used as a JOIN. + */ + function asView() { + $unique = spl_object_hash($this); + $classname = "QueryView{$unique}"; + $class = <<<EOF +class {$classname} extends VerySimpleModel { + static \$meta = array( + 'view' => true, + ); + static \$queryset; + + static function getQuery(\$compiler) { + return ' ('.static::\$queryset->getQuery().') '; + } +} +EOF; + eval($class); // Ugh + $classname::$queryset = $this; + return $classname; + } + function serialize() { $info = get_object_vars($this); unset($info['query']); @@ -1681,6 +1716,15 @@ class MySqlCompiler extends SqlCompiler { $vals = array_map(array($this, 'input'), $b); $b = implode(', ', $vals); } + // MySQL doesn't support LIMIT or OFFSET in subqueries. Instead, add + // the query as a JOIN and add the join constraint into the WHERE + // clause. + elseif ($b instanceof QuerySet && $b->isWindowed()) { + $f1 = $b->values[0]; + $view = $b->asView(); + $alias = $this->pushJoin($view, $a, $view, array('constraint'=>array())); + return sprintf('%s = %s.%s', $a, $alias, $this->quote($f1)); + } else { $b = $this->input($b); } @@ -1744,12 +1788,17 @@ class MySqlCompiler extends SqlCompiler { if ($extra instanceof Q) { $constraints[] = $this->compileQ($extra, $model, self::SLOT_JOINS); } + if (!isset($rmodel)) + $rmodel = $model; // Support inline views $table = ($rmodel::$meta['view']) + // XXX: Support parameters from the nested query ? $rmodel::getQuery($this) : $this->quote($rmodel::$meta['table']); - return $join.$table - .' '.$alias.' ON ('.implode(' AND ', $constraints).')'; + $base = "$join$table $alias"; + if ($constraints) + $base .= ' ON ('.implode(' AND ', $constraints).')'; + return $base; } /** @@ -1820,7 +1869,7 @@ class MySqlCompiler extends SqlCompiler { } if (isset($queryset->extra['where'])) { foreach ($queryset->extra['where'] as $S) { - $where[] = '('.$S.')'; + $where[] = "($S)"; } } if ($where) @@ -1992,7 +2041,7 @@ class MySqlCompiler extends SqlCompiler { foreach ($queryset->distinct as $d) list($group_by[]) = $this->getField($d, $model); } - $group_by = $group_by ? ' GROUP BY '.implode(',', $group_by) : ''; + $group_by = $group_by ? ' GROUP BY '.implode(', ', $group_by) : ''; $joins = $this->getJoins($queryset); diff --git a/include/class.search.php b/include/class.search.php index 84d3bbbcdc7d4478be5d1af5e733eadda3e7a323..355feb0725ce8b0fea9d9f5e1ba46c269dc312e0 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -304,7 +304,7 @@ class MysqlSearchBackend extends SearchBackend { $key = 'COALESCE(Z1.ticket_id, Z2.ticket_id)'; $criteria->extra(array( 'select' => array( - 'key' => $key, + 'ticket_id' => $key, 'relevance'=>'`search`.`relevance`', ), 'order_by' => array(new SqlCode('`relevance`')), @@ -702,6 +702,7 @@ class SavedSearch extends VerySimpleModel { function mangleQuerySet(QuerySet $qs, $form=false) { $form = $form ?: $this->getForm(); $searchable = $this->getCurrentSearchFields($form->getSource()); + $qs = clone $qs; // Figure out fields to search on foreach ($form->getFields() as $f) { diff --git a/include/staff/templates/tickets.tmpl.php b/include/staff/templates/tickets.tmpl.php index 7fc45a41afd9328dc2b3300b16b935385e57fa76..c2d1702f5d4ca5a2935fcce111f6a536f2d46cc5 100644 --- a/include/staff/templates/tickets.tmpl.php +++ b/include/staff/templates/tickets.tmpl.php @@ -64,7 +64,7 @@ if ($results) { ?> <?php csrf_token(); ?> <input type="hidden" name="a" value="mass_process" > <input type="hidden" name="do" id="action" value="" > - <table class="list fixed" border="0" cellspacing="1" cellpadding="2" width="940"> + <table class="list" border="0" cellspacing="1" cellpadding="2" width="940"> <thead> <tr> <?php @@ -78,11 +78,11 @@ if ($results) { ?> <th width="380"><?php echo __('Subject'); ?></th> <?php if ($user) { ?> - <th width="150"><?php echo __('Department'); ?></th> - <th width="150"><?php echo __('Assignee'); ?></th> + <th width="125"><?php echo __('Department'); ?></th> + <th width="125"><?php echo __('Assignee'); ?></th> <?php } else { ?> - <th width="300"><?php echo __('User'); ?></th> + <th width="250"><?php echo __('User'); ?></th> <?php } ?> </tr> @@ -100,8 +100,8 @@ if ($results) { ?> $assigned=''; if ($T['staff_id']) $assigned = new PersonsName(array( - 'first' => $row['staff__firstname'], - 'last' => $row['staff__lastname'] + 'first' => $T['staff__firstname'], + 'last' => $T['staff__lastname'] )); elseif ($T['team_id']) $assigned = Team::getLocalById($T['team_id'], 'name', $T['team__name']); @@ -128,28 +128,28 @@ if ($results) { ?> title="<?php echo __('Preview Ticket'); ?>" href="tickets.php?id=<?php echo $T['ticket_id']; ?>" data-preview="#tickets/<?php echo $T['ticket_id']; ?>/preview"><?php echo $tid; ?></a></td> - <td align="center" nowrap><?php echo Format::datetime($row['effective_date']); ?></td> + <td align="center" nowrap><?php echo Format::datetime($T['effective_date']); ?></td> <td><?php echo $status; ?></td> - <td><a <?php if ($flag) { ?> class="Icon <?php echo $flag; ?>Ticket" title="<?php echo ucfirst($flag); ?> Ticket" <?php } ?> - href="tickets.php?id=<?php echo $T['ticket_id']; ?>"><span class="truncate" - style="max-width: 290px"><?php echo $subject; ?></span></a> + <td><a class="truncate <?php if ($flag) { ?> Icon <?php echo $flag; ?>Ticket" title="<?php echo ucfirst($flag); ?> Ticket<?php } ?>" + style="max-width: 230px;" + href="tickets.php?id=<?php echo $T['ticket_id']; ?>"><?php echo $subject; ?></a> <?php if ($threadcount > 1) { ?> - <span class="pull-right faded-more"><i class="icon-comments-alt"></i> - <small><?php echo $threadcount; ?></small> + <span class="pull-right faded-more"><i class="icon-comments-alt"></i> + <small><?php echo $threadcount; ?></small></span> <?php } - if ($row['attachments']) + if ($T['attachments']) echo '<i class="small icon-paperclip icon-flip-horizontal"></i>'; - if ($row['collaborators']) + if ($T['collaborators']) echo '<i class="icon-group faded-more"></i>'; ?> - </td> + </span></td> <?php if ($user) { $dept = Dept::getLocalById($T['dept_id'], 'name', $T['dept__name']); ?> - <td><span class="truncate" style="max-wdith:150px"><?php + <td><span class="truncate" style="max-wdith:125px"><?php echo Format::htmlchars($dept); ?></span></td> - <td><span class="truncate" style="max-width:150px"><?php + <td><span class="truncate" style="max-width:125px"><?php echo Format::htmlchars($assigned); ?></span></td> <?php } else { ?> diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index ca01e03ddb0383ba6451abeeffcf2a85dba5f0b5..edb325b288a66eab97a80b29283d15ddff8fca98 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -16,6 +16,20 @@ unset($args['a']); $refresh_url = $path . '?' . http_build_query($args); +$sort_options = array( + 'priority,updated' => __('Priority + Most Recently Updated'), + 'updated' => __('Most Recently Updated'), + 'priority,created' => __('Priority + Most Recently Created'), + 'due' => __('Due Soon'), + 'priority,due' => __('Priority + Due Soon'), + 'number' => __('Ticket Number'), + 'answered' => __('Most Recently Answered'), + 'closed' => __('Most Recently Closed'), + 'hot' => __('Longest Thread'), + 'relevance' => __('Relevance'), +); +$use_subquery = true; + $queue_name = strtolower($_GET['status'] ?: $_GET['a']); //Status is overloaded switch ($queue_name) { case 'closed': @@ -23,23 +37,32 @@ case 'closed': $results_type=__('Closed Tickets'); $showassigned=true; //closed by. $tickets->values('staff__firstname', 'staff__lastname', 'team__name', 'team_id'); + $queue_sort_options = array('closed', 'priority,due', 'due', + 'priority,updated', 'priority,created', 'answered', 'number', 'hot'); break; case 'overdue': $status='open'; $results_type=__('Overdue Tickets'); $tickets->filter(array('isoverdue'=>1)); + $queue_sort_options = array('priority,due', 'due', 'priority,updated', + 'updated', 'answered', 'priority,created', 'number', 'hot'); break; case 'assigned': $status='open'; $staffId=$thisstaff->getId(); $results_type=__('My Tickets'); $tickets->filter(array('staff_id'=>$thisstaff->getId())); + $queue_sort_options = array('updated', 'priority,updated', + 'priority,created', 'priority,due', 'due', 'answered', 'number', + 'hot'); break; case 'answered': $status='open'; $showanswered=true; $results_type=__('Answered Tickets'); $tickets->filter(array('isanswered'=>1)); + $queue_sort_options = array('answered', 'priority,updated', 'updated', + 'priority,created', 'priority,due', 'due', 'number', 'hot'); break; default: case 'search': @@ -55,7 +78,9 @@ case 'search': else { $tickets = $tickets->filter(Q::any(array( 'number__startswith' => $_REQUEST['query'], + 'user__name__contains' => $_REQUEST['query'], 'user__emails__address__contains' => $_REQUEST['query'], + 'user__org__name__contains' => $_REQUEST['query'], ))); } break; @@ -66,7 +91,25 @@ case 'search': $tickets = $search->mangleQuerySet($tickets, $form); $view_all_tickets = $thisstaff->getRole()->hasPerm(SearchBackend::PERM_EVERYTHING); $results_type=__('Advanced Search') - . '<a class="action-button" href="?clear_filter"><i class="icon-ban-circle"></i> <em>' . __('clear') . '</em></a>'; + . '<a class="action-button" href="?clear_filter"><i style="top:0" class="icon-ban-circle"></i> <em>' . __('clear') . '</em></a>'; + $queue_sort_options = array('priority,updated', 'priority,created', + 'priority,due', 'due', 'updated', 'answered', + 'closed', 'number', 'hot'); + $has_relevance = false; + foreach ($tickets->getSortFields() as $sf) { + if ($sf instanceof SqlCode && $sf->code == '`relevance`') { + $has_relevance = true; + break; + } + } + if ($has_relevance) { + $use_subquery = false; + array_unshift($queue_sort_options, 'relevance'); + } + elseif ($_SESSION[$queue_sort_key] == 'relevance') { + unset($_SESSION[$queue_sort_key]); + } + break; } // Fall-through and show open tickets @@ -80,6 +123,9 @@ case 'open': $tickets->filter(Q::any(array('staff_id'=>0, 'team_id'=>0))); else $tickets->values('staff__firstname', 'staff__lastname', 'team__name'); + $queue_sort_options = array('priority,updated', 'updated', + 'priority,due', 'due', 'priority,created', 'answered', 'number', + 'hot'); break; } @@ -97,45 +143,55 @@ if ($status) // ------------------------------------------------------------ if (!$view_all_tickets) { // -- Open and assigned to me + $assigned = Q::any(array( + 'staff_id' => $thisstaff->getId(), + )); + // -- Open and assigned to a team of mine + if ($teams = array_filter($thisstaff->getTeams())) + $assigned->add(array('team_id__in' => $teams)); + $visibility = array( - new Q(array('status__state'=>'open', 'staff_id' => $thisstaff->getId())) + new Q(array('status__state'=>'open', $assigned)) ); // -- Routed to a department of mine if (!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts())) $visibility[] = new Q(array('dept_id__in' => $depts)); - // -- Open and assigned to a team of mine - if (($teams = $thisstaff->getTeams()) && count(array_filter($teams))) - $visibility[] = new Q(array( - 'team_id__in' => array_filter($teams), 'status__state'=>'open' - )); + $tickets->filter(Q::any($visibility)); } -// Add in annotations -$tickets->annotate(array( - 'collab_count' => SqlAggregate::COUNT('thread__collaborators'), - 'attachment_count' => SqlAggregate::COUNT('thread__entries__attachments'), - 'thread_count' => SqlAggregate::COUNT('thread__entries'), -)); - -// Select pertinent columns -// ------------------------------------------------------------ -$tickets->values('lock__staff_id', 'staff_id', 'isoverdue', 'team_id', 'ticket_id', 'number', 'cdata__subject', 'user__default_email__address', 'source', 'cdata__:priority__priority_color', 'cdata__:priority__priority_desc', 'status_id', 'status__name', 'status__state', 'dept_id', 'dept__name', 'user__name', 'lastupdate'); +// TODO :: Apply requested quick filter -// Apply requested quick filter +// Apply requested pagination +$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; +$pageNav = new Pagenate($tickets->count(), $page, PAGE_LIMIT); +$pageNav->setURL('tickets.php', $args); +$tickets = $pageNav->paginate($tickets); // Apply requested sorting $queue_sort_key = sprintf(':Q:%s:sort', $queue_name); -if (isset($_GET['sort'])) +if (isset($_GET['sort'])) { $_SESSION[$queue_sort_key] = $_GET['sort']; +} +elseif (!isset($_SESSION[$queue_sort_key])) { + $_SESSION[$queue_sort_key] = $queue_sort_options[0]; +} + switch ($_SESSION[$queue_sort_key]) { case 'number': $tickets->extra(array( 'order_by'=>array(SqlExpression::times(new SqlField('number'), 1)) )); break; + +case 'priority,created': + $tickets->order_by('cdata__:priority__priority_urgency'); + // Fall through to columns for `created` case 'created': + $date_header = __('Date Created'); + $date_col = 'created'; + $tickets->values('created'); $tickets->order_by('-created'); break; @@ -146,26 +202,77 @@ case 'due': $date_header = __('Due Date'); $date_col = 'est_duedate'; $tickets->values('est_duedate'); - $tickets->filter(array('est_duedate__isnull'=>false)); - $tickets->order_by('est_duedate'); + $tickets->order_by(SqlFunction::COALESCE(new SqlField('est_duedate'), 'zzz')); + break; + +case 'closed': + $date_header = __('Date Closed'); + $date_col = 'closed'; + $tickets->values('closed'); + $tickets->order_by('-closed'); + break; + +case 'answered': + $date_header = __('Last Response'); + $date_col = 'lastresponse'; + $date_fallback = '<em class="faded">'.__('unanswered').'</em>'; + $tickets->order_by('-lastresponse'); + $tickets->values('lastresponse'); + break; + +case 'hot': + $tickets->order_by('-thread_count'); + $tickets->annotate(array( + 'thread_count' => SqlAggregate::COUNT('thread__entries'), + )); + break; + +case 'relevance': + $tickets->order_by(new SqlCode('relevance')); break; default: +case 'priority,updated': + $tickets->order_by('cdata__:priority__priority_urgency'); + // Fall through for columns defined for `updated` case 'updated': - $tickets->order_by('cdata__:priority__priority_urgency', '-lastupdate'); + $date_header = __('Last Updated'); + $date_col = 'lastupdate'; + $tickets->order_by('-lastupdate'); break; } -// Apply requested pagination -$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; -$pageNav=new Pagenate($tickets->count(), $page, PAGE_LIMIT); -$pageNav->setURL('tickets.php', $args); -$tickets = $pageNav->paginate($tickets); + +// Rewrite $tickets to use a nested query, which will include the LIMIT part +// in order to speed the result +// +// ATM, advanced search with keywords doesn't support the subquery approach +if ($use_subquery) { + $orig_tickets = clone $tickets; + $tickets2 = TicketModel::objects(); + $tickets2->values = $tickets->values; + $tickets2->filter(array('ticket_id__in' => $tickets->values_flat('ticket_id'))); + + // Transfer the order_by from the original tickets + $tickets2->order_by($tickets->getSortFields()); + $tickets = $tickets2; +} TicketForm::ensureDynamicDataView(); +// Select pertinent columns +// ------------------------------------------------------------ +$tickets->values('lock__staff_id', 'staff_id', 'isoverdue', 'team_id', 'ticket_id', 'number', 'cdata__subject', 'user__default_email__address', 'source', 'cdata__:priority__priority_color', 'cdata__:priority__priority_desc', 'status_id', 'status__name', 'status__state', 'dept_id', 'dept__name', 'user__name', 'lastupdate'); + +// Add in annotations +$tickets->annotate(array( + 'collab_count' => SqlAggregate::COUNT('thread__collaborators'), + 'attachment_count' => SqlAggregate::COUNT('thread__entries__attachments'), + 'thread_count' => SqlAggregate::COUNT('thread__entries'), +)); + // Save the query to the session for exporting -$_SESSION[':Q:tickets'] = $tickets; +$_SESSION[':Q:tickets'] = $orig_tickets; ?> @@ -199,14 +306,12 @@ $_SESSION[':Q:tickets'] = $tickets; <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( - 'updated' => __('Most Recently Updated'), - 'created' => __('Most Recently Created'), - 'due' => __('Due Soon'), - 'priority,due' => __('Priority + Due Soon'), - 'number' => __('Ticket Number'), -) as $mode => $desc) { ?> + <select name="sort" onchange="javascript: $.pjax({ + url:'?' + addSearchParam('sort', $(this).val()), + timeout: 2000, + container: '#pjax-container'});"> +<?php foreach ($queue_sort_options as $mode) { + $desc = $sort_options[$mode]; ?> <option value="<?php echo $mode; ?>" <?php if ($mode == $_SESSION[$queue_sort_key]) echo 'selected="selected"'; ?>><?php echo $desc; ?></option> <?php } ?> </select> @@ -230,7 +335,7 @@ $_SESSION[':Q:tickets'] = $tickets; <input type="hidden" name="do" id="action" value="" > <input type="hidden" name="status" value="<?php echo Format::htmlchars($_REQUEST['status'], true); ?>" > - <table class="list fixed" border="0" cellspacing="1" cellpadding="2" width="940"> + <table class="list" border="0" cellspacing="1" cellpadding="2" width="940"> <thead> <tr> <?php if ($thisstaff->canManageTickets()) { ?> @@ -239,7 +344,7 @@ $_SESSION[':Q:tickets'] = $tickets; <th width="70"> <?php echo __('Ticket'); ?></th> <th width="100"> - <?php echo $date_header ?: __('Date'); ?></th> + <?php echo $date_header ?: __('Date Created'); ?></th> <th width="280"> <?php echo __('Subject'); ?></th> <th width="170"> @@ -291,17 +396,14 @@ $_SESSION[':Q:tickets'] = $tickets; $flag='overdue'; $lc=''; - $dept = Dept::getLocalById($T['dept_id'], 'name', $T['dept__name']); - if($showassigned) { - if($T['staff_id']) - $lc=sprintf('<span class="Icon staffAssigned truncate">%s</span>',(string) new PersonsName($T['staff__firstname'].' '.$T['staff__lastname'])); - elseif($T['team_id']) - $lc=sprintf('<span class="Icon teamAssigned">%s</span>', - Team::getLocalById($T['team_id'], 'name', $T['team__name'])); - else - $lc=' '; - }else{ - $lc='<span class="truncate">'.Format::htmlchars($dept).'</span>'; + if ($showassigned) { + if ($T['staff_id']) + $lc = new PersonsName($T['staff__firstname'].' '.$T['staff__lastname']); + elseif ($T['team_id']) + $lc = Team::getLocalById($T['team_id'], 'name', $T['team__name']); + } + else { + $lc = Dept::getLocalById($T['dept_id'], 'name', $T['dept__name']); } $tid=$T['number']; $subject = $subject_field->display($subject_field->to_php($T['cdata__subject'])); @@ -328,25 +430,27 @@ $_SESSION[':Q:tickets'] = $tickets; href="tickets.php?id=<?php echo $T['ticket_id']; ?>" data-preview="#tickets/<?php echo $T['ticket_id']; ?>/preview" ><?php echo $tid; ?></a></td> - <td align="center" nowrap><?php echo Format::datetime($T[$date_col ?: 'lastupdate']); ?></td> + <td align="center" nowrap><?php echo Format::datetime($T[$date_col ?: 'lastupdate']) ?: $date_fallback; ?></td> <td><a <?php if ($flag) { ?> class="Icon <?php echo $flag; ?>Ticket" title="<?php echo ucfirst($flag); ?> Ticket" <?php } ?> - style="max-width: 80%; max-width: calc(100% - 86px);" + style="max-width: 210px;" href="tickets.php?id=<?php echo $T['ticket_id']; ?>"><span class="truncate"><?php echo $subject; ?></span></a> - <?php - if ($threadcount>1) - echo "<small>($threadcount)</small> ".'<i - class="icon-fixed-width icon-comments-alt"></i> '; - if ($T['collab_count']) - echo '<i class="icon-fixed-width icon-group faded"></i> '; - if ($T['attachment_count']) - echo '<i class="icon-fixed-width icon-paperclip"></i> '; - ?> +<?php if ($T['attachment_count']) + echo '<i class="small icon-paperclip icon-flip-horizontal"></i>'; + if ($threadcount > 1) { ?> + <span class="pull-right faded-more"><i class="icon-comments-alt"></i> + <small><?php echo $threadcount; ?></small> + </span> + <?php } ?> </td> - <td nowrap><span class="truncate"><?php + <td nowrap><div><?php + if ($T['collab_count']) + echo '<span class="pull-right faded-more"><i class="icon-group"></i></span>'; + ?><span class="truncate" style="max-width:<?php + echo $T['collab_count'] ? '150px' : '170px'; ?>"><?php $un = new PersonsName($T['user__name']); echo Format::htmlchars($un); - ?></td> + ?></span></div></td> <?php if($search && !$status){ $displaystatus=TicketStatus::getLocalById($T['status_id'], 'value', $T['status__name']); @@ -359,7 +463,7 @@ $_SESSION[':Q:tickets'] = $tickets; <?php } ?> - <td nowrap> <?php echo $lc; ?></td> + <td nowrap> <?php echo Format::htmlchars($lc); ?></td> </tr> <?php } //end of foreach diff --git a/scp/css/scp.css b/scp/css/scp.css index 68188bee7044962c36c077f62de59da1250092e1..c64d284d8fdf27bf2676339fd90845872d03e918 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -58,6 +58,17 @@ div#header a { .faded { color:#666; } +.faded-more { + color: #aaa; + color: rgba(0,0,0,0.4); +} + +.small[class^="icon-"], +.small[class*=" icon-"] { + vertical-align: baseline; + padding-left: 2px; + font-size: 80%; +} .strike { text-decoration:line-through; color:red; } @@ -377,6 +388,7 @@ a.lists { background:url(../images/icons/icon-list.png); background-size: 16px 1 background-repeat: no-repeat; min-height: 16px; display: inline-block; + vertical-align: middle; } diff --git a/scp/js/scp.js b/scp/js/scp.js index 0a0e577a809f840360570946e65bcae3bb371e76..79d14e3da1ecd8c5f06d16ad2f0630d890666ba7 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -901,5 +901,5 @@ function addSearchParam(key, value) { if(i<0) {kvp[kvp.length] = [key,value].join('=');} //this will reload the page, it's likely better to store this until finished - document.location.search = kvp.join('&'); + return kvp.join('&'); }