diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 9e8bce892bc6230dff7c9cfb920f2d26198e3ca4..c27914de5f49b603284a60d2604409c0291e0e27 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -1428,5 +1428,36 @@ function refer($tid, $target=null) { include STAFFINC_DIR . 'templates/task-view.tmpl.php'; } + + + function export($id) { + global $thisstaff; + + if (is_numeric($id)) + $queue = SavedSearch::lookup($id); + else + $queue = AdhocSearch::load($id); + + if (!$thisstaff) + Http::response(403, 'Agent login is required'); + elseif (!$queue || !$queue->checkAccess($thisstaff)) + Http::response(404, 'No such saved queue'); + + if ($_POST && is_array($_POST['fields'])) { + // Cache export preferences + $id = $queue->getId(); + $_SESSION['Export:Q'.$id]['fields'] = $_POST['fields']; + $_SESSION['Export:Q'.$id]['filename'] = $_POST['filename']; + $_SESSION['Export:Q'.$id]['delimiter'] = $_POST['delimiter']; + + if ($queue->isSaved() && isset($_POST['save-changes'])) + $queue->updateExports(array_flip($_POST['fields'])); + + Http::response(201, 'Export Ready'); + } + + include STAFFINC_DIR . 'templates/queue-export.tmpl.php'; + + } } ?> diff --git a/include/class.export.php b/include/class.export.php index 6b7b4be5d54319e6f886046540f0b1daf31f4070..1ca9e3f47e1ef84878b89bd9da33c647439336c0 100644 --- a/include/class.export.php +++ b/include/class.export.php @@ -40,7 +40,8 @@ class Export { # SQL is exported, but for something like tickets, we will need to # export attached messages, reponses, and notes, as well as # attachments associated with each, ... - static function dumpTickets($sql, $target=array(), $how='csv') { + static function dumpTickets($sql, $target=array(), $how='csv', + $options=array()) { // Add custom fields to the $sql statement $cdata = $fields = array(); foreach (TicketForm::getInstance()->getFields() as $f) { @@ -90,13 +91,15 @@ class Export { } } return $record; - }) + }, + 'delimiter' => @$options['delimiter']) ); } - static function saveTickets($sql, $fields, $filename, $how='csv') { + static function saveTickets($sql, $fields, $filename, $how='csv', + $options=array()) { Http::download($filename, "text/$how"); - self::dumpTickets($sql, $fields, $how); + self::dumpTickets($sql, $fields, $how, $options); exit; } @@ -316,16 +319,17 @@ class ResultSetExporter { class CsvResultsExporter extends ResultSetExporter { - function dump() { - if (!$this->output) - $this->output = fopen('php://output', 'w'); + function getDelimiter() { + + if (isset($this->options['delimiter'])) + return $this->options['delimiter']; // Detect delimeter from the current locale settings. For locales // which use comma (,) as the decimal separator, the semicolon (;) // should be used as the field separator $delimiter = ','; - if (class_exists('NumberFormatter')) { + if (!$this->options['delimiter'] && class_exists('NumberFormatter')) { $nf = NumberFormatter::create(Internationalization::getCurrentLocale(), NumberFormatter::DECIMAL); $s = $nf->getSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL); @@ -333,6 +337,17 @@ class CsvResultsExporter extends ResultSetExporter { $delimiter = ';'; } + return $delimiter; + } + + + function dump() { + + if (!$this->output) + $this->output = fopen('php://output', 'w'); + + + $delimiter = $this->getDelimiter(); // Output a UTF-8 BOM (byte order mark) fputs($this->output, chr(0xEF) . chr(0xBB) . chr(0xBF)); fputcsv($this->output, $this->getHeaders(), $delimiter); diff --git a/include/class.queue.php b/include/class.queue.php index c2a5487a1aa950e41472fd76a81a8085cc3a95bb..740673590b3a7531b9d284985d916021e41d43b6 100644 --- a/include/class.queue.php +++ b/include/class.queue.php @@ -711,7 +711,7 @@ class CustomQueue extends VerySimpleModel { )); } - function export($filename, $format) { + function export($options=array()) { if (!($query=$this->getBasicQuery())) return false; @@ -719,7 +719,31 @@ class CustomQueue extends VerySimpleModel { if (!($fields=$this->getExportFields())) return false; - return Export::saveTickets($query, $fields, $filename, $format); + + $filename = sprintf('%s Tickets-%s.csv', + $this->getName(), + strftime('%Y%m%d')); + // See if we have cached export preference + if (isset($_SESSION['Export:Q'.$this->getId()])) { + $opts = $_SESSION['Export:Q'.$this->getId()]; + if (isset($opts['fields'])) + $fields = array_intersect_key($fields, + array_flip($opts['fields'])); + if (isset($opts['filename']) + && ($parts = pathinfo($opts['filename']))) { + $filename = $opts['filename']; + if (strcasecmp($parts['extension'], 'csv') !=0) + $filename ="$filename.csv"; + } + + if (isset($opts['delimiter'])) + $options['delimiter'] = $opts['delimiter']; + + } + + + return Export::saveTickets($query, $fields, $filename, 'csv', + $options); } /** @@ -917,6 +941,59 @@ class CustomQueue extends VerySimpleModel { $this->clearFlag(self::FLAG_DISABLED); } + function updateExports($fields, $save=true) { + + if (!$fields) + return false; + + $order = array_keys($fields); + // Filter exportable fields + if (!($fields = array_intersect_key($this->getExportableFields(), $fields))) + return false; + + $new = $fields; + foreach ($this->exports as $f) { + $key = $f->getPath(); + if (!isset($fields[$key])) { + $this->exports->remove($f); + continue; + } + + $info = $fields[$key]; + if (is_array($info)) + $heading = $info['heading']; + else + $heading = $info; + + $f->set('heading', $heading); + $f->set('sort', array_search($key, $order)+1); + unset($new[$key]); + } + + foreach ($new as $k => $field) { + if (is_array($field)) + $heading = $field['heading']; + else + $heading = $field; + + $f = QueueExport::create(array( + 'path' => $k, + 'heading' => $heading, + 'sort' => array_search($k, $order)+1)); + $this->exports->add($f); + } + + $this->exports->sort(function($f) { return $f->sort; }); + + if (!count($this->exports) && $this->parent) + $this->hasFlag(self::FLAG_INHERIT_EXPORT); + + if ($save) + $this->exports->saveAll(); + + return true; + } + function update($vars, &$errors=array()) { // Set basic search information if (!$vars['name']) @@ -1001,29 +1078,7 @@ class CustomQueue extends VerySimpleModel { // Update export fields for the queue if (isset($vars['exports']) && !$this->hasFlag(self::FLAG_INHERIT_EXPORT)) { - $new = $vars['exports']; - $order = array_keys($new); - foreach ($this->exports as $f) { - $key = $f->getPath(); - if (!isset($vars['exports'][$key])) { - $this->exports->remove($f); - continue; - } - $info = $vars['exports'][$key]; - $f->set('heading', $info['heading']); - $f->set('sort',array_search($key, $order)); - unset($new[$key]); - } - - foreach($new as $k => $field) { - $f = QueueExport::create(array( - //'queue_id' => $this->getId(), - 'path' => $k, - 'heading' => $field['heading'], - 'sort' => array_search($k, $order))); - $this->exports->add($f); - } - $this->exports->sort(function($f) { return $f->sort; }); + $this->updateExports($vars['exports'], false); } if (!count($this->exports) && $this->parent) diff --git a/include/class.search.php b/include/class.search.php index c2e4f93d1f1e03c226b65eec1c87cad27ec78852..e454da06adb0ad90319e41a5c9187ac70ea44586 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -652,6 +652,10 @@ class SavedSearch extends CustomQueue { // Override the ORM relationship to force no children private $children = false; + function isSaved() { + return true; + } + static function forStaff(Staff $agent) { return static::objects()->filter(Q::any(array( 'staff_id' => $agent->getId(), @@ -679,9 +683,38 @@ class SavedSearch extends CustomQueue { class AdhocSearch extends SavedSearch { + + function isSaved() { + return false; + } + + function checkAccess($staff) { + return true; + } + function getName() { return $this->title ?: $this->describeCriteria(); } + + function load($key) { + + if (strpos($key, 'adhoc') === 0) + list(, $key) = explode(',', $key, 2); + + if (!$key + || !isset($_SESSION['advsearch']) + || !($config=$_SESSION['advsearch'][$key])) + return null; + + $queue = new AdhocSearch(array( + 'id' => "adhoc,$key", + 'root' => 'T', + 'title' => __('Advanced Search'), + )); + $queue->config = $config; + + return $queue; + } } class AdvancedSearchForm extends SimpleForm { diff --git a/include/staff/templates/advanced-search.tmpl.php b/include/staff/templates/advanced-search.tmpl.php index c2cd1c1d442a833807b9adbcf6aeb8c4d4c36d0a..5cee3f051097b552c880c0dcd551d60427fed2ee 100644 --- a/include/staff/templates/advanced-search.tmpl.php +++ b/include/staff/templates/advanced-search.tmpl.php @@ -51,7 +51,7 @@ foreach ($queues as $id => $name) { <div class="tab_content" id="criteria"> <div class="flex row"> - <div class="span12"> + <div class="span12" style="overflow-y: scroll; height:100%;"> <?php if ($parent) { ?> <div class="faded" style="margin-bottom: 1em"> <div> @@ -69,12 +69,14 @@ foreach ($queues as $id => $name) { </div> -<div class="tab_content hidden" id="columns"> +<div class="tab_content hidden" id="columns" style="overflow-y: scroll; +height:100%;"> <?php include STAFFINC_DIR . "templates/queue-columns.tmpl.php"; ?> </div> -<div class="tab_content hidden" id="fields"> +<div class="tab_content hidden" id="fields" style="overflow-y: scroll; +height:auto;"> <?php include STAFFINC_DIR . "templates/queue-fields.tmpl.php"; ?> </div> diff --git a/include/staff/templates/queue-export.tmpl.php b/include/staff/templates/queue-export.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..c2659bceac67bfb837c710a1fce1f3af815e7b33 --- /dev/null +++ b/include/staff/templates/queue-export.tmpl.php @@ -0,0 +1,132 @@ +<?php +$qname = $queue->getName() ?: __('Tickets Export'); + +// Session cache +$cache = $_SESSION['Export:Q'.$queue->getId()]; +if (isset($cache['filename'])) + $filename = $cache['filename']; +else + $filename = trim(sprintf('%s Tickets - %s.csv', + Format::htmlchars($queue->getName() ?: ''), + strftime('%Y%m%d'))); + +if (isset($cache['delimiter'])) + $delimiter = $cache['delimiter']; +else + $delimiter = ''; //TODO: get user's preference (browswer settings) + +$fields = $queue->getExportFields(false) ?: array(); +if (isset($cache['fields']) && $fields) + $fields = array_intersect_key($fields, array_flip($cache['fields'])); + +?> +<div id="tickets-export"> +<h3 class="drag-handle"><?php echo Format::htmlchars($qname); ?></h3> +<a class="close" href=""><i class="icon-remove-circle"></i></a> +<hr/> +<form action="#tickets/export/<?php echo $queue->getId(); ?>" method="post" name="export"> + <div style="overflow-y: auto; height:400px; margin-bottom:5px;"> + <table class="table"> + <tbody> + <tr class="header"> + <td><small><i class="icon-caret-down"></i> <?php echo + sprintf('%s <strong class="faded">( <span id="fields-count">%d</span> %s )</strong>', + __('Check columns to export'), + count($fields), + __('selected')); + ?> </small></td> + </tr> + </tbody> + <tbody class="sortable-rows" id="fields"> + <?php + foreach ($queue->getExportableFields() as $path => $label) { + echo sprintf('<tr style="display: table-row;"> + <td><i class="faded-more + icon-sort"></i> <label><input + type="checkbox" name="fields[]" value="%s" %s> + <span>%s</span></label><td></tr>', + $path, + isset($fields[$path]) ? 'checked="checked"' : '', + @$fields[$path] ?: $label); + } ?> + </tbody> + </table> + </div> + <?php + if ($queue->isSaved()) { ?> + <div id="save-changes" class="hidden" style="padding-top:5px; border-top: 1px dotted #ddd;"> + <span><i class="icon-bell-alt" style="color:red;"></i> + <label><input type="checkbox" name='save-changes' > Save export preference changes</label> </span> + </div> + <?php + } ?> + <div style="margin-top:10px;"><small><a href="#" + id="more"><i class="icon-caret-right"></i> <?php echo __('Advanced CSV Options'); ?></a></small></div> + <div id="more-options" class="hidden" style="padding:5px; border-top: 1px dotted #777;"> + <div><span class="faded" style="width:60px;"><?php echo __('Filename'); ?>: </span><input + name="filename" type="text" size="40" + value="<?php echo Format::htmlchars($filename); ?>"></div> + <div><span class="faded" style="width:60px;"><?php echo __('Delimiter'); ?>: </span><input + name="csv-delimiter" type="text" maxlength="1" size=10 + value="<?php echo $delimiter; ?>" + placeholder=", (Comma)" maxlength="1" /></div> + </div> + <p class="full-width"> + <span class="buttons pull-left"> + <input type="reset" id="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 __('Export'); ?>"> + </span> + </p> +</form> +</div> +<div class="clear"></div> +<script> ++function() { + // Return a helper with preserved width of cells + var fixHelper = function(e, ui) { + ui.children().each(function() { + $(this).width($(this).width()); + }); + return ui; + }; + // Sortable tables for dynamic forms objects + $('.sortable-rows').sortable({ + 'helper': fixHelper, + 'cursor': 'move', + 'stop': function(e, ui) { + $('div#save-changes').fadeIn(); + } + }); + + $('#more').click(function() { + var more = $(this); + $('#more-options').slideToggle('fast', function(){ + if ($(this).is(":hidden")) + more.find('i').removeClass('icon-caret-down').addClass('icon-caret-right'); + else + more.find('i').removeClass('icon-caret-right').addClass('icon-caret-down'); + }); + return false; + }); + + $(document).on('change', 'tbody#fields input:checkbox', function (e) { + var f = $(this).closest('form'); + var count = $("input[name='fields[]']:checked", f).length; + $('div#save-changes', f).fadeIn(); + $('span#fields-count', f).html(count); + }); + + $(document).on('click', 'input#reset', function(e) { + var f = $(this).closest('form'); + $('input.save-changes', f).prop('checked', false); + $('span#fields-count', f).html(<?php echo count($fields); ?>); + $('div#save-changes', f).hide(); + }); + +}(); +</script> diff --git a/include/staff/templates/queue-tickets.tmpl.php b/include/staff/templates/queue-tickets.tmpl.php index 0c8fe8c9c617be7969f386f52ceb65adb3a2876b..9c45da9370a20658185faffd0c22120a2caae588 100644 --- a/include/staff/templates/queue-tickets.tmpl.php +++ b/include/staff/templates/queue-tickets.tmpl.php @@ -296,15 +296,24 @@ foreach ($tickets as $T) { <span class="faded pull-right"><?php echo $pageNav->showing(); ?></span> <?php echo __('Page').':'.$pageNav->getPageLinks().' '; - echo sprintf('<a class="export-csv no-pjax" href="?%s">%s</a>', - Http::build_query(array( - 'a' => 'export', 'queue' => $_REQUEST['queue'], - 'status' => $_REQUEST['status'])), - __('Export')); ?> + <a href="#tickets/export/<?php echo $queue->getId(); ?>" id="queue-export" class="no-pjax" + ><?php echo __('Export'); ?></a> <i class="help-tip icon-question-sign" href="#export"></i> </div> <?php } ?> - </form> +<script type="text/javascript"> +$(function() { + $(document).on('click', 'a#queue-export', function(e) { + e.preventDefault(); + var url = 'ajax.php/'+$(this).attr('href').substr(1) + $.dialog(url, 201, function (xhr) { + window.location.href = '?a=export&queue=<?php echo $queue->getId(); ?>'; + return false; + }); + return false; + }); +}); +</script> diff --git a/scp/ajax.php b/scp/ajax.php index cdb5fa561107fca49e874137ae3adc02a938f412..bc23cb1ebca158bc75d7386a72b51496374dcbb1 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -170,6 +170,8 @@ $dispatcher = patterns('', url('^(?P<tid>\d+)/refer(?:/(?P<to>\w+))?$', 'refer'), url('^(?P<tid>\d+)/referrals$', 'referrals'), url('^(?P<tid>\d+)/claim$', 'claim'), + url('^export/(?P<id>\d+)$', 'export'), + url('^export/adhoc,(?P<key>[\w=/+]+)$', 'export'), url('^search', patterns('ajax.search.php:SearchAjaxAPI', url_get('^$', 'getAdvancedSearchDialog'), url_post('^$', 'doSearch'), diff --git a/scp/tickets.php b/scp/tickets.php index 18a7e0721dd267eb96fd17e00132ffe1a7155127..841a5df79b30ce7680332212cfe8d44b9cca9fad 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -102,13 +102,8 @@ if (!$ticket) { reset($_SESSION['advsearch']); $key = key($_SESSION['advsearch']); } - // XXX: De-duplicate and simplify this code - $queue = new AdhocSearch(array( - 'id' => "adhoc,$key", - 'root' => 'T', - 'title' => __('Advanced Search'), - )); - $queue->config = $_SESSION['advsearch'][$key]; + + $queue = AdhocSearch::load($key); } // Make the current queue sticky @@ -541,9 +536,7 @@ if($ticket) { $inc = 'ticket-open.inc.php'; elseif ($_REQUEST['a'] == 'export' && $queue) { // XXX: Check staff access? - $filename = sprintf('%s Tickets-%s', $queue->getName(), - strftime('%Y%m%d')); - if (!$queue->export($filename, 'csv')) + if (!$queue->export()) $errors['err'] = __('Unable to export results.') .' '.__('Internal error occurred'); } elseif ($queue) {