diff --git a/assets/default/css/theme.css b/assets/default/css/theme.css index ba4fc8e02c8e1db4b67936aaa174a6522b6ba3ca..e2120f71909cc711027c791bd0573437bdb04adb 100644 --- a/assets/default/css/theme.css +++ b/assets/default/css/theme.css @@ -118,6 +118,12 @@ fieldset { a { color: #0072bc; text-decoration: none; + display: inline-block; + margin-bottom: 1px; +} +a:hover { + border-bottom: 1px dotted #0072bc; + margin-bottom: 0; } h1 { color: #00AEEF; @@ -789,9 +795,6 @@ label.required, span.required { font-size: 1em; background-image: url('../images/icons/thread.gif?1319556657'); } -.Icon:hover { - text-decoration: underline; -} #ticketTable { border: 1px solid #aaa; border-left: none; @@ -828,25 +831,22 @@ label.required, span.required { #ticketTable tr.alt td { background: #f9f9f9; } -#ticketSearchForm { - display: inline-block; - float: left; - padding: 0 0 5px 0; +i.refresh { + color: #0a0; + font-size: 80%; + vertical-align: middle; } -a.refresh { - display: block; - width: auto; - float: right; - height: 20px; - line-height: 20px; - text-align: center; - padding: 0 10px 0 28px; - border: 1px solid #aaa; - margin-left: 10px; - color: #333; - background-position: 5px 50%; - background-repeat: no-repeat; - background-image: url('../images/icons/refresh.png'); +.states small { + font-size: 70%; +} +.active.state { + font-weight: bold; +} +.search.well { + padding: 10px; + background-color: rgba(0,0,0,0.05); + margin-bottom: 10px; + margin-top: -15px; } .infoTable { background: #F4FAFF; @@ -1153,6 +1153,10 @@ img.avatar { .thread-body .attachments .filesize { margin-left: 0.5em; } +.thread-body .attachments a, +.thread-body .attachments a:hover { + text-decoration: none; +} .thread-body .attachment-info { margin-right: 10px; display: inline-block; diff --git a/include/class.client.php b/include/class.client.php index eb4ef00a870c62a6355d3269ec5b6c0476ed27c5..bbd6270c5f7ee781f58f2335ea29e20c895bb687 100644 --- a/include/class.client.php +++ b/include/class.client.php @@ -245,6 +245,10 @@ class EndUser extends BaseAuthenticatedUser { return ($stats=$this->getTicketStats())?$stats['closed']:0; } + function getNumTopicTickets($topic_id) { + return ($stats=$this->getTicketStats())?$stats['topics'][$topic_id]:0; + } + function getNumOrganizationTickets() { if (!($stats=$this->getTicketStats())) return 0; @@ -272,58 +276,20 @@ class EndUser extends BaseAuthenticatedUser { } private function getStats() { - - $where = ' WHERE ticket.user_id = '.db_input($this->getId()) - .' OR collab.user_id = '.db_input($this->getId()).' '; - - $where2 = ' WHERE user.org_id > 0 AND user.org_id = '.db_input($this->getOrgId()).' '; - - $join = 'LEFT JOIN '.THREAD_TABLE.' thread - ON (ticket.ticket_id = thread.object_id and thread.object_type = \'T\') - LEFT JOIN '.THREAD_COLLABORATOR_TABLE.' collab - ON (collab.thread_id=thread.id - AND collab.user_id = '.db_input($this->getId()).' ) '; - - $sql = 'SELECT \'open\', count( ticket.ticket_id ) AS tickets ' - .'FROM ' . TICKET_TABLE . ' ticket ' - .'INNER JOIN '.TICKET_STATUS_TABLE. ' status - ON (ticket.status_id=status.id - AND status.state=\'open\') ' - . $join - . $where - - .'UNION SELECT \'closed\', count( ticket.ticket_id ) AS tickets ' - .'FROM ' . TICKET_TABLE . ' ticket ' - .'INNER JOIN '.TICKET_STATUS_TABLE. ' status - ON (ticket.status_id=status.id - AND status.state=\'closed\' ) ' - . $join - . $where - - .'UNION SELECT \'org-open\', count( ticket.ticket_id ) AS tickets ' - .'FROM ' . TICKET_TABLE . ' ticket ' - .'INNER JOIN '.USER_TABLE.' user ON (ticket.user_id = user.id) ' - .'INNER JOIN '.TICKET_STATUS_TABLE. ' status - ON (ticket.status_id=status.id - AND status.state=\'open\' ) ' - . $join - . $where2 - - .'UNION SELECT \'org-closed\', count( ticket.ticket_id ) AS tickets ' - .'FROM ' . TICKET_TABLE . ' ticket ' - .'INNER JOIN '.USER_TABLE.' user ON (ticket.user_id = user.id) ' - .'INNER JOIN '.TICKET_STATUS_TABLE. ' status - ON (ticket.status_id=status.id - AND status.state=\'closed\' ) ' - . $join - . $where2; - - $res = db_query($sql); - $stats = array(); - while($row = db_fetch_row($res)) { - $stats[$row[0]] = $row[1]; + $basic = Ticket::objects() + ->annotate(array('count' => SqlAggregate::COUNT('ticket_id'))) + ->values('status__state', 'topic_id') + ->filter(Q::any(array( + 'user_id' => $this->getId(), + 'thread__collaborators__user_id' => $this->getId(), + ))); + + $stats = array('open' => 0, 'closed' => 0, 'topics' => array()); + foreach ($basic as $row) { + $stats[$row['status__state']] += $row['count']; + if ($row['topic_id']) + $stats['topics'][$row['topic_id']] += $row['count']; } - return $stats; } diff --git a/include/class.orm.php b/include/class.orm.php index cd8992ae881e1917e18ad89cd6014282f5bdc639..3157c0a8497d93351ba911ddae3935b88f44e122 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -1096,7 +1096,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl // Aggregate works like annotate, except that it sets up values // fetching which will disable model creation $this->annotate($annotations); - $this->values_flat(); + $this->values(); // Disable other fields from being fetched $this->aggregated = true; $this->related = false; @@ -2267,7 +2267,7 @@ class MySqlCompiler extends SqlCompiler { } } // If no group by has been set yet, use the root model pk - if (!$group_by) { + if (!$group_by && !$queryset->aggregated) { foreach ($model::getMeta('pk') as $pk) $group_by[] = $rootAlias .'.'. $pk; } diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php index 58d056a1786b624bb3ea0e63e5c10ae62cc24d80..12908ece25e65bc1f0240ca6ca88522973a01744 100644 --- a/include/client/tickets.inc.php +++ b/include/client/tickets.inc.php @@ -1,29 +1,33 @@ <?php if(!defined('OSTCLIENTINC') || !is_object($thisclient) || !$thisclient->isValid()) die('Access Denied'); +$settings = &$_SESSION['client:Q']; + +// Unpack search, filter, and sort requests +if (isset($_REQUEST['clear'])) + $settings = array(); +if (isset($_REQUEST['keywords'])) + $settings['keywords'] = $_REQUEST['keywords']; +if (isset($_REQUEST['topic_id'])) + $settings['topic_id'] = $_REQUEST['topic_id']; +if (isset($_REQUEST['status'])) + $settings['status'] = $_REQUEST['status']; + $tickets = TicketModel::objects(); $qs = array(); $status=null; -if(isset($_REQUEST['status'])) { //Query string status has nothing to do with the real status used below. - $qs += array('status' => $_REQUEST['status']); - //Status we are actually going to use on the query...making sure it is clean! - $status=strtolower($_REQUEST['status']); - switch(strtolower($_REQUEST['status'])) { - case 'open': - $results_type=__('Open Tickets'); - $tickets->filter(array('status__state'=>'open')); - break; - case 'closed': - $results_type=__('Closed Tickets'); - $tickets->filter(array('status__state'=>'closed')); + +if ($settings['status']) + $status = strtolower($settings['status']); + switch ($status) { + default: + $status = 'open'; + case 'open': + case 'closed': + $results_type = ($status == 'closed') ? __('Closed Tickets') : __('Open Tickets'); + $tickets->filter(array('status__state' => $status)); break; - default: - $status=''; //ignore - } -} elseif($thisclient->getNumOpenTickets()) { - $status='open'; //Defaulting to open - $results_type=__('Open Tickets'); } $sortOptions=array('id'=>'number', 'subject'=>'cdata__subject', @@ -49,17 +53,20 @@ $tickets->filter(Q::any(array( ))); // Perform basic search -$search=($_REQUEST['a']=='search' && $_REQUEST['q']); -if($search) { - $qs += array('a' => $_REQUEST['a'], 'q' => $_REQUEST['q']); - if (is_numeric($_REQUEST['q'])) { - $tickets->filter(array('number__startswith'=>$_REQUEST['q'])); +if ($settings['keywords']) { + $q = $settings['keywords']; + if (is_numeric($q)) { + $tickets->filter(array('number__startswith'=>$q)); } else { //Deep search! // Use the search engine to perform the search - $tickets = $ost->searcher->find($_REQUEST['q'], $tickets); + $tickets = $ost->searcher->find($q, $tickets); } } +if ($settings['topic_id']) { + $tickets = $tickets->filter(array('topic_id' => $settings['topic_id'])); +} + TicketForm::ensureDynamicDataView(); $total=$tickets->count(); @@ -89,28 +96,62 @@ $tickets->values( ); ?> -<h1><?php echo __('Tickets');?></h1> -<br> +<div class="search well"> +<div class="flush-left"> <form action="tickets.php" method="get" id="ticketSearchForm"> <input type="hidden" name="a" value="search"> - <input type="text" name="q" size="20" value="<?php echo Format::htmlchars($_REQUEST['q']); ?>"> - <select name="status"> - <option value="">— <?php echo __('Any Status');?> —</option> - <option value="open" - <?php echo ($status=='open') ? 'selected="selected"' : '';?>> - <?php echo _P('ticket-status', 'Open');?> (<?php echo $thisclient->getNumOpenTickets(); ?>)</option> - <?php - if($thisclient->getNumClosedTickets()) { - ?> - <option value="closed" - <?php echo ($status=='closed') ? 'selected="selected"' : '';?>> - <?php echo __('Closed');?> (<?php echo $thisclient->getNumClosedTickets(); ?>)</option> - <?php - } ?> + <input type="search" name="keywords" size="30" value="<?php echo Format::htmlchars($settings['keywords']); ?>"> + <input type="submit" value="<?php echo __('Search');?>"> +<div class="pull-right"> + <?php echo __('Help Topic'); ?>: + <select name="topic_id" class="nowarn" onchange="javascript: this.form.submit(); "> + <option value="">— <?php echo __('All Help Topics');?> —</option> +<?php foreach (Topic::getHelpTopics(true) as $id=>$name) { + $count = $thisclient->getNumTopicTickets($id); + if ($count == 0) + continue; +?> + <option value="<?php echo $id; ?>"i + <?php if ($settings['topic_id'] == $id) echo 'selected="selected"'; ?> + ><?php echo sprintf('%s (%d)', Format::htmlchars($name), + $thisclient->getNumTopicTickets($id)); ?></option> +<?php } ?> </select> - <input type="submit" value="<?php echo __('Go');?>"> +</div> </form> -<a class="refresh" href="<?php echo Format::htmlchars($_SERVER['REQUEST_URI']); ?>"><?php echo __('Refresh'); ?></a> +</div> + +<?php if ($settings['keywords'] || $settings['topic_id'] || $_REQUEST['sort']) { ?> +<div style="margin-top:10px"><strong><a href="?clear" style="color:#777"><i class="icon-remove-circle"></i> <?php echo __('Clear all filters and sort'); ?></a></strong></div> +<?php } ?> + +</div> + + +<h1 style="margin:10px 0"> + <a href="<?php echo Format::htmlchars($_SERVER['REQUEST_URI']); ?>" + ><i class="refresh icon-refresh"></i> + <?php echo __('Tickets'); ?> + </a> + +<div class="pull-right states"> + <small> + <i class="icon-file-alt"></i> + <a class="state <?php if ($status == 'open') echo 'active'; ?>" + href="?<?php echo Http::build_query(array('a' => 'search', 'status' => 'open')); ?>"> + <?php echo sprintf('%s (%d)', _P('ticket-status', 'Open'), $thisclient->getNumOpenTickets()); ?> + </a> + + <span style="color:lightgray">|</span> + + <i class="icon-file-text"></i> + <a class="state <?php if ($status == 'closed') echo 'active'; ?>" + href="?<?php echo Http::build_query(array('a' => 'search', 'status' => 'closed')); ?>"> + <?php echo sprintf('%s (%d)', __('Closed'), $thisclient->getNumClosedTickets()); ?> + </a> + </small> +</div> +</h1> <table id="ticketTable" width="800" border="0" cellspacing="0" cellpadding="0"> <caption><?php echo $showing; ?></caption> <thead> @@ -170,7 +211,7 @@ $tickets->values( } } else { - echo '<tr><td colspan="6">'.__('Your query did not match any records').'</td></tr>'; + echo '<tr><td colspan="5">'.__('Your query did not match any records').'</td></tr>'; } ?> </tbody> diff --git a/include/client/view.inc.php b/include/client/view.inc.php index 498e6d6aecd75a1aaca5e61a432ee731e1e0f12f..2be53bc477943706361921063d776169e1398dce 100644 --- a/include/client/view.inc.php +++ b/include/client/view.inc.php @@ -31,9 +31,9 @@ if ($thisclient && $thisclient->isGuest() <tr> <td colspan="2" width="100%"> <h1> + <a href="tickets.php?id=<?php echo $ticket->getId(); ?>" title="<?php echo __('Reload'); ?>"><i class="refresh icon-refresh"></i></a> <b><?php echo $ticket->getSubject(); ?></b> <small>#<?php echo $ticket->getNumber(); ?></small> - <a href="tickets.php?id=<?php echo $ticket->getId(); ?>" title="<?php echo __('Reload'); ?>"><span class="Icon refresh"> </span></a> <div class="pull-right"> <a class="action-button" href="tickets.php?a=print&id=<?php echo $ticket->getId(); ?>"><i class="icon-print"></i> <?php echo __('Print'); ?></a> diff --git a/js/osticket.js b/js/osticket.js index 0da43e7d94a9836687caf0d45dcdcbe27fa26df8..d885b9216e66a2111639312e7d54ce8f0102618d 100644 --- a/js/osticket.js +++ b/js/osticket.js @@ -21,7 +21,7 @@ $(document).ready(function(){ left : ($(window).width() / 2 - 160) }); - $("form :input").change(function() { + $(document).on('change', "form :input:not(.nowarn)", function() { var fObj = $(this).closest('form'); if(!fObj.data('changed')){ fObj.data('changed', true); @@ -30,7 +30,7 @@ $(document).ready(function(){ return __("Are you sure you want to leave? Any changes or info you've entered will be discarded!"); }); } - }); + }); $("form :input[type=reset]").click(function() { var fObj = $(this).closest('form');