diff --git a/bootstrap.php b/bootstrap.php index a4e56510c61a44e676040d6e20a54f2b89abb583..8eb90fd17abbe5c76a1d575cad473fda11eaaed6 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -117,6 +117,7 @@ class Bootstrap { define('FILTER_RULE_TABLE', $prefix.'filter_rule'); define('PLUGIN_TABLE', $prefix.'plugin'); + define('SEQUENCE_TABLE', $prefix.'sequence'); define('API_KEY_TABLE',$prefix.'api_key'); define('TIMEZONE_TABLE',$prefix.'timezone'); @@ -323,8 +324,6 @@ define('THISPAGE', Misc::currentURL()); define('DEFAULT_MAX_FILE_UPLOADS',ini_get('max_file_uploads')?ini_get('max_file_uploads'):5); define('DEFAULT_PRIORITY_ID',1); -define('EXT_TICKET_ID_LEN',6); //Ticket create. when you start getting collisions. Applies only on random ticket ids. - #Global override if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) // Take the left-most item for X-Forwarded-For diff --git a/include/ajax.sequence.php b/include/ajax.sequence.php new file mode 100644 index 0000000000000000000000000000000000000000..37be03269c87483a77e8d99cc7aaf840ffaeae34 --- /dev/null +++ b/include/ajax.sequence.php @@ -0,0 +1,106 @@ +<?php + +require_once(INCLUDE_DIR . 'class.sequence.php'); + +class SequenceAjaxAPI extends AjaxController { + + /** + * Ajax: GET /sequence/<id> + * + * Fetches the current value of a sequence + * + * Get-Arguments: + * format - (string) format string used to format the current value of + * the sequence. + * + * Returns: + * (string) Current sequence number, optionally formatted + * + * Throws: + * 403 - Not logged in + * 404 - Unknown sequence id + * 422 - Invalid sequence id + */ + function current($id) { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Login required'); + elseif ($id == 0) + $sequence = new RandomSequence(); + elseif (!$id || !is_numeric($id)) + Http::response(422, 'Id is required'); + elseif (!($sequence = Sequence::lookup($id))) + Http::response(404, 'No such object'); + + return $sequence->current($_GET['format']); + } + + /** + * Ajax: GET|POST /sequence/manage + * + * Gets a dialog box content or updates data from the content + * + * Post-Arguments: + * seq[<id>][*] - Updated information for existing sequences + * seq[<new-*>[*] - Information for new sequences + * seq[<id>][deleted] - If set to true, indicates that the sequence + * should be deleted from the database + * + * Throws: + * 403 - Not logged in + * 422 - Information sent for update of unknown sequence + */ + function manage() { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Login required'); + + $sequences = Sequence::objects()->all(); + $info = array( + 'action' => '#sequence/manage', + ); + + $valid = true; + if ($_POST) { + foreach ($_POST['seq'] as $id=>$info) { + if (strpos($id, 'new-') === 0) { + unset($info['id']); + $sequences[] = Sequence::create($info); + } + else { + foreach ($sequences as $s) { + if ($s->id == $id) + break; + $s = false; + } + if (!$s) { + Http::response(422, $id . ': Invalid or unknown sequence'); + } + elseif ($info['deleted']) { + $s->delete(); + continue; + } + foreach ($info as $f=>$val) { + if (isset($s->{$f})) + $s->set($f, $val); + elseif ($f == 'current') + $s->next = $val; + } + if (($v = $s->isValid()) !== true) { + $msg = sprintf('%s: %s', $s->getName(), $valid); + $valid = false; + } + } + } + if ($valid) { + foreach ($sequences as $s) + $s->save(); + Http::response(205, 'All sequences updated'); + } + } + + include STAFFINC_DIR . 'templates/sequence-manage.tmpl.php'; + } +} diff --git a/include/class.config.php b/include/class.config.php index 0616bb96b1a6ac569efceed44db7f82880da78d7..79bb05fd4b354dd50e5ce167b69591c4e13a6ae6 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -624,8 +624,20 @@ class OsticketConfig extends Config { return true; //No longer an option...hint: big plans for headers coming!! } - function useRandomIds() { - return ($this->get('random_ticket_ids')); + function getDefaultSequence() { + if ($this->get('sequence_id')) + $sequence = Sequence::lookup($this->get('sequence_id')); + if (!$sequence) + $sequence = new RandomSequence(); + return $sequence; + } + function getDefaultNumberFormat() { + return $this->get('number_format'); + } + function getNewTicketNumber() { + $s = $this->getDefaultSequence(); + return $s->next($this->getDefaultNumberFormat(), + array('Ticket', 'isTicketNumberUnique')); } /* autoresponders & Alerts */ @@ -976,7 +988,8 @@ class OsticketConfig extends Config { $this->update('default_storage_bk', $vars['default_storage_bk']); return $this->updateAll(array( - 'random_ticket_ids'=>$vars['random_ticket_ids'], + 'number_format'=>$vars['number_format'] ?: '######', + 'sequence_id'=>$vars['sequence_id'] ?: 0, 'default_priority_id'=>$vars['default_priority_id'], 'default_help_topic'=>$vars['default_help_topic'], 'default_sla_id'=>$vars['default_sla_id'], diff --git a/include/class.orm.php b/include/class.orm.php index 1d8810919a1bab31a9ca853e88162a86d9de3902..aa05a040d4b03b827958df1e2e107cdba1c3ba2e 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -260,6 +260,12 @@ class SqlFunction { $args = (count($this->args)) ? implode(',', db_input($this->args)) : ""; return sprintf('%s(%s)', $this->func, $args); } + + static function __callStatic($func, $args) { + $I = new static($func); + $I->args = $args; + return $I; + } } class QuerySet implements IteratorAggregate, ArrayAccess { @@ -272,6 +278,10 @@ class QuerySet implements IteratorAggregate, ArrayAccess { var $offset = 0; var $related = array(); var $values = array(); + var $lock = false; + + const LOCK_EXCLUSIVE = 1; + const LOCK_SHARED = 2; var $compiler = 'MySqlCompiler'; var $iterator = 'ModelInstanceIterator'; @@ -299,6 +309,11 @@ class QuerySet implements IteratorAggregate, ArrayAccess { return $this; } + function lock($how=false) { + $this->lock = $how ?: self::LOCK_EXCLUSIVE; + return $this; + } + function limit($count) { $this->limit = $count; return $this; @@ -932,6 +947,14 @@ class MySqlCompiler extends SqlCompiler { $sql .= ' LIMIT '.$queryset->limit; if ($queryset->offset) $sql .= ' OFFSET '.$queryset->offset; + switch ($queryset->lock) { + case QuerySet::LOCK_EXCLUSIVE: + $sql .= ' FOR UPDATE'; + break; + case QuerySet::LOCK_SHARED: + $sql .= ' LOCK IN SHARE MODE'; + break; + } return new MysqlExecutor($sql, $this->params); } diff --git a/include/class.sequence.php b/include/class.sequence.php new file mode 100644 index 0000000000000000000000000000000000000000..2569718737b711f9fb86d779da2aefb115b6feda --- /dev/null +++ b/include/class.sequence.php @@ -0,0 +1,214 @@ +<?php + +require_once INCLUDE_DIR . 'class.orm.php'; + +class Sequence extends VerySimpleModel { + + static $meta = array( + 'table' => SEQUENCE_TABLE, + 'pk' => array('id'), + 'ordering' => array('name'), + ); + + const FLAG_INTERNAL = 0x0001; + + /** + * Function: next + * + * Fetch the next number in the sequence. The next number in the + * sequence will be adjusted in the database so that subsequent calls to + * this function should never receive the same result. + * + * Optionally, a format specification can be sent to the function and + * the next sequence number will be returned padded. See the `::format` + * function for more details. + * + * Optionally, a check callback can be specified to ensure the next + * value of the sequence is valid. This might be useful for a + * pseudo-random generator which might repeat existing numbers. The + * callback should have the following signature and should return + * boolean TRUE to approve the number. + * + * Parameters: + * $format - (string) Format specification for the result + * $check - (function($format, $next)) Validation callback function + * where $next will be the next value as an integer, and $formatted + * will be the formatted version of the number, if a $format + * parameter were passed to the `::next` method. + * + * Returns: + * (int|string) - next number in the sequence, optionally formatted and + * verified. + */ + function next($format=false, $check=false) { + $digits = $format ? $this->getDigitCount($format) : false; + do { + $next = $this->__next($digits); + $formatted = $format ? $this->format($format, $next) : $next; + } + while ($check && !$check($formatted, $next)); + return $formatted; + } + + /** + * Function: current + * + * Peeks at the next number in the sequence without incrementing the + * sequence. + * + * Parameters: + * $format - (string:optional) format string to receive the current + * sequence number + * + * Returns: + * (int|string) - the next number in the sequence without advancing the + * sequence, optionally formatted. See the `::format` method for + * formatting details. + */ + function current($format=false) { + return $format ? $this->format($format, $this->next) : $this->next; + } + + /** + * Function: format + * + * Formats a number to the given format. The number will be placed into + * the format string according to the locations of hash characters (#) + * in the string. If more hash characters are encountered than digits + * the digits are left-padded accoring to the sequence padding + * character. If fewer are found, the last group will receive all the + * remaining digits. + * + * Hash characters can be escaped with a backslash (\#) and will emit a + * single hash character to the output. + * + * Parameters: + * $format - (string) Format string for the number, e.g. "TX-######-US" + * $number - (int) Number to appear in the format. If not + * specified the next number in this sequence will be used. + */ + function format($format, $number) { + $groups = array(); + preg_match_all('/(?<!\\\)#+/', $format, $groups, PREG_OFFSET_CAPTURE); + + $total = 0; + foreach ($groups[0] as $g) + $total += strlen($g[0]); + + $number = str_pad($number, $total, $this->padding, STR_PAD_LEFT); + $output = ''; + $start = $noff = 0; + // Interate through the ### groups and replace the number of hash + // marks with numbers from the sequence + foreach ($groups[0] as $g) { + $size = strlen($g[0]); + // Add format string from previous marker to current ## group + $output .= str_replace('\#', '#', + substr($format, $start, $g[1] - $start)); + // Add digits from the sequence number + $output .= substr($number, $noff, $size); + // Set offset counts for the next loop + $start = $g[1] + $size; + $noff += $size; + } + // If there are more digits of number than # marks, add the number + // where the last hash mark was found + if (strlen($number) > $noff) + $output .= substr($number, $noff); + // Add format string from ending ## group + $output .= str_replace('\#', '#', substr($format, $start)); + return $output; + } + + function getDigitCount($format) { + $total = 0; + $groups = array(); + + return preg_match_all('/(?<!\\\)#/', $format, $groups); + } + + /** + * Function: __next + * + * Internal implementation of the next number generator. This method + * will lock the database object backing to protect against concurent + * ticket processing. The lock will be released at the conclusion of the + * session. + * + * Parameters: + * $digits - (int:optional) number of digits (size) of the number. This + * is useful for random sequences which need a size hint to + * generate a "next" value. + * + * Returns: + * (int) - The current number in the sequence. The sequence is advanced + * and assured to be session-wise atomic before the value is returned. + */ + function __next($digits=false) { + // Lock the database object -- this is important to handle concurrent + // requests for new numbers + static::objects()->filter(array('id'=>$this->id))->lock()->one(); + + // Increment the counter + $next = $this->next; + $this->next += $this->increment; + $this->updated = SqlFunction::NOW(); + $this->save(); + + return $next; + } + + function hasFlag($flag) { + return $this->flags & $flag != 0; + } + function setFlag($flag, $value=true) { + if ($value) + $this->flags |= $flag; + else + $this->flags &= ~$flag; + } + + function getName() { + return $this->name; + } + + function isValid() { + if (!$this->name) + return 'Name is required'; + if (!$this->increment) + return 'Non-zero increment is required'; + if (!$this->next || $this->next < 0) + return 'Positive "next" value is required'; + + if (!$this->padding) + $this->padding = '0'; + + return true; + } + + function __get($what) { + // Pseudo-property for $sequence->current + if ($what == 'current') + return $this->current(); + return parent::__get($what); + } +} + +class RandomSequence extends Sequence { + var $padding = '0'; + + // Override the ORM constructor and do nothing + function __construct() {} + + function __next($digits=6) { + return Misc::randNumber($digits); + } + + function current($format=false) { + return $this->next($format); + } + + function save() { + throw new RuntimeException('RandomSequence is not database-backed'); + } +} diff --git a/include/class.thread.php b/include/class.thread.php index 3b773bfda402c0633e5aab07a96e5e7ab840e469..8c5920597a9b3bf7d811917d47e89f9cfccb0277 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -896,9 +896,9 @@ Class ThreadEntry { $match = array(); if ($subject && $mailinfo['email'] - && preg_match("/#(?:[\p{L}-]+)?([0-9]{1,10})/u", $subject, $match) + && preg_match("/\b#(\S+)/u", $subject, $match) //Lookup by ticket number - && ($ticket = Ticket::lookupByNumber((int)$match[1])) + && ($ticket = Ticket::lookupByNumber($match[1])) //Lookup the user using the email address && ($user = User::lookup(array('emails__address' => $mailinfo['email'])))) { //We have a valid ticket and user diff --git a/include/class.ticket.php b/include/class.ticket.php index 6518d12eae5722401bb1f96a4932ed00e14473c0..1e8be21e29a27b422d6ada677f8af4a6be364cfc 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -2081,16 +2081,9 @@ class Ticket { return self::lookup(self:: getIdByNumber($number, $email)); } - function genRandTicketNumber($len = EXT_TICKET_ID_LEN) { - - //We can allow collissions...number and email must be unique ...so - // same number with diff emails is ok.. But for clarity...we are going to make sure it is unique. - $number = Misc::randNumber($len); - if(db_num_rows(db_query('SELECT ticket_id FROM '.TICKET_TABLE.' - WHERE `number`='.db_input($number)))) - return Ticket::genRandTicketNumber($len); - - return $number; + static function isTicketNumberUnique($number) { + return 0 == db_num_rows(db_query( + 'SELECT ticket_id FROM '.TICKET_TABLE.' WHERE `number`='.db_input($number))); } function getIdByMessageId($mid, $email) { @@ -2470,7 +2463,7 @@ class Ticket { $ipaddress = $vars['ip'] ?: $_SERVER['REMOTE_ADDR']; //We are ready son...hold on to the rails. - $number = Ticket::genRandTicketNumber(); + $number = $topic ? $topic->getNewTicketNumber() : $cfg->getNewTicketNumber(); $sql='INSERT INTO '.TICKET_TABLE.' SET created=NOW() ' .' ,lastmessage= NOW()' .' ,user_id='.db_input($user->getId()) @@ -2493,13 +2486,6 @@ class Ticket { /* -------------------- POST CREATE ------------------------ */ - if(!$cfg->useRandomIds()) { - //Sequential ticket number support really..really suck arse. - //To make things really easy we are going to use autoincrement ticket_id. - db_query('UPDATE '.TICKET_TABLE.' SET `number`='.db_input($id).' WHERE ticket_id='.$id.' LIMIT 1'); - //TODO: RETHING what happens if this fails?? [At the moment on failure random ID is used...making stuff usable] - } - // Save the (common) dynamic form $form->setTicketId($id); $form->save(); diff --git a/include/class.topic.php b/include/class.topic.php index ee0f2a47ede9eab61d87bf0955c5aec9fcd8bd30..940d9156b232a210042c1566bebcf771c8f36f51 100644 --- a/include/class.topic.php +++ b/include/class.topic.php @@ -14,6 +14,8 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ +require_once INCLUDE_DIR . 'class.sequence.php'; + class Topic { var $id; @@ -27,6 +29,8 @@ class Topic { const FORM_USE_PARENT = 4294967295; + const FLAG_CUSTOM_NUMBERS = 0x0001; + function Topic($id) { $this->id=0; $this->load($id); @@ -183,7 +187,28 @@ class Topic { } function getInfo() { - return $this->getHashtable(); + $base = $this->getHashtable(); + $base['custom-numbers'] = $this->hasFlag(self::FLAG_CUSTOM_NUMBERS); + return $base; + } + + function hasFlag($flag) { + return $this->ht['flags'] & $flag != 0; + } + + function getNewTicketNumber() { + global $cfg; + + if (!$this->hasFlag(self::FLAG_CUSTOM_NUMBERS)) + return $cfg->getNewTicketNumber(); + + if ($this->ht['sequence_id']) + $sequence = Sequence::lookup($this->ht['sequence_id']); + if (!$sequence) + $sequence = new RandomSequence(); + + return $sequence->next($this->ht['number_format'] ?: '######', + array('Ticket', 'isTicketNumberUnique')); } function setSortOrder($i) { @@ -332,6 +357,9 @@ class Topic { .',page_id='.db_input($vars['page_id']) .',isactive='.db_input($vars['isactive']) .',ispublic='.db_input($vars['ispublic']) + .',sequence_id='.db_input($vars['custom-numbers'] ? $vars['sequence_id'] : 0) + .',number_format='.db_input($vars['custom-numbers'] ? $vars['number_format'] : '') + .',flags='.db_input($vars['custom-numbers'] ? self::FLAG_CUSTOM_NUMBERS : 0) .',noautoresp='.db_input(isset($vars['noautoresp']) && $vars['noautoresp']?1:0) .',notes='.db_input(Format::sanitize($vars['notes'])); diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php index 2c7a9f2317743e6ff170236a06ec39ce2355df23..07ed570405dd8d57a565893a78c400254e795dc1 100644 --- a/include/client/tickets.inc.php +++ b/include/client/tickets.inc.php @@ -125,22 +125,21 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting <caption><?php echo $showing; ?></caption> <thead> <tr> - <th width="70" nowrap> + <th nowrap> <a href="tickets.php?sort=ID&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Ticket ID">Ticket #</a> </th> - <th width="100"> + <th width="120"> <a href="tickets.php?sort=date&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Date">Create Date</a> </th> - <th width="80"> + <th width="100"> <a href="tickets.php?sort=status&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Status">Status</a> </th> - <th width="300"> + <th width="320"> <a href="tickets.php?sort=subj&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Subject">Subject</a> </th> - <th width="150"> + <th width="120"> <a href="tickets.php?sort=dept&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Department">Department</a> </th> - <th width="100">Phone Number</th> </tr> </thead> <tbody> @@ -158,12 +157,9 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting $subject="<b>$subject</b>"; $ticketNumber="<b>$ticketNumber</b>"; } - $phone=Format::phone($row['phone']); - if($row['phone_ext']) - $phone.=' '.$row['phone_ext']; ?> <tr id="<?php echo $row['ticket_id']; ?>"> - <td class="centered"> + <td> <a class="Icon <?php echo strtolower($row['source']); ?>Ticket" title="<?php echo $row['email']; ?>" href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $ticketNumber; ?></a> </td> @@ -173,13 +169,12 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting <a href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $subject; ?></a> </td> <td> <?php echo Format::truncate($dept,30); ?></td> - <td><?php echo $phone; ?></td> </tr> <?php } } else { - echo '<tr><td colspan="7">Your query did not match any records</td></tr>'; + echo '<tr><td colspan="6">Your query did not match any records</td></tr>'; } ?> </tbody> diff --git a/include/i18n/en_US/config.yaml b/include/i18n/en_US/config.yaml index 9d1e385c3bd979cf2bd12dcfeb6f6f9aa874085b..a63764ba9c1b3b1aeb8b56488c0cbc6c4aa4409e 100644 --- a/include/i18n/en_US/config.yaml +++ b/include/i18n/en_US/config.yaml @@ -78,7 +78,8 @@ core: hide_staff_name: 0 overlimit_notice_active: 0 email_attachments: 1 - random_ticket_ids: 1 + number_format: '######' + sequence_id: 0 log_level: 2 log_graceperiod: 12 client_registration: 'public' diff --git a/include/i18n/en_US/help/tips/manage.helptopic.yaml b/include/i18n/en_US/help/tips/manage.helptopic.yaml index d2179170cea229fe773d361c85080c4e15c73188..0297f1ceeb93f418b112cdfecbc4213f1ae2cbeb 100644 --- a/include/i18n/en_US/help/tips/manage.helptopic.yaml +++ b/include/i18n/en_US/help/tips/manage.helptopic.yaml @@ -112,3 +112,10 @@ ticket_auto_response: links: - title: Autoresponder Settings href: /scp/settings.php?t=autoresp + +custom_numbers: + title: Custom Ticket Numbers + content: > + Choose "Custom" here to override the system default ticket numbering + format for tickets created in this help topic. See the help tips on + the Settings / Tickets page for more details on the settings. diff --git a/include/i18n/en_US/help/tips/settings.ticket.yaml b/include/i18n/en_US/help/tips/settings.ticket.yaml index 50693c2a370d1c4b061c96f58e4bab9d271eebe7..e694af2b391a87f3df213a351ee0ec4baa77656b 100644 --- a/include/i18n/en_US/help/tips/settings.ticket.yaml +++ b/include/i18n/en_US/help/tips/settings.ticket.yaml @@ -13,6 +13,23 @@ # must match the HTML #ids put into the page template. # --- +number_format: + title: Ticket Number Format + content: > + This setting is used to create numbers for new tickets. Use hash + signs (`#`) where numbers are to be replaced. For example, for + six-digit numbers, use <code>######</code>. Any other text in the + number format will be preserved. Number formats can be overridden by + each help topic. + +sequence_id: + title: Ticket Number Sequence + content: + Choose a sequence from which to derive new ticket numbers. The + system has a incrementing sequence and a random sequence by default. + You may create as many sequences as you wish. Use various sequences + in the number format configuration for help topics. + default_sla: title: Default SLA content: > diff --git a/include/mysqli.php b/include/mysqli.php index e8bfeb32ebcfa7f6acd5fe26991a6fc07b32c136..899e4ef1807b9e34da57ea6fd0ba023e6045d234 100644 --- a/include/mysqli.php +++ b/include/mysqli.php @@ -76,6 +76,19 @@ function db_connect($host, $user, $passwd, $options = array()) { @db_set_variable('sql_mode', ''); + // Start a new transaction -- for performance. Transactions are always + // committed at shutdown (below) + $__db->autocommit(false); + + // Auto commit the transaction at shutdown and re-enable statement-level + // autocommit + register_shutdown_function(function() { + global $__db, $err; + if (!$__db->commit()) + $err = 'Unable to save changes to database'; + $__db->autocommit(true); + }); + // Use connection timing to seed the random number generator Misc::__rand_seed((microtime(true) - $start) * 1000000); diff --git a/include/staff/helptopic.inc.php b/include/staff/helptopic.inc.php index 00f06d38d0ca604b2adf81daa606132110fd7359..a13daf551d7fe1c323f17cb5e5a5b858e5364ee4 100644 --- a/include/staff/helptopic.inc.php +++ b/include/staff/helptopic.inc.php @@ -241,6 +241,61 @@ if ($info['form_id'] == Topic::FORM_USE_PARENT) echo 'selected="selected"'; <i class="help-tip icon-question-sign" href="#ticket_auto_response"></i> </td> </tr> + <tr> + <td> + Ticket Number Format: + </td> + <td> + <label> + <input type="radio" name="custom-numbers" value="0" <?php echo !$info['custom-numbers']?'checked="checked"':''; ?> + onchange="javascript:$('#custom-numbers').hide();"> System Default + </label> <label> + <input type="radio" name="custom-numbers" value="1" <?php echo $info['custom-numbers']?'checked="checked"':''; ?> + onchange="javascript:$('#custom-numbers').show(200);"> Custom + </label> <i class="help-tip icon-question-sign" href="#custom_numbers"></i> + </td> + </tr> + </tbody> + <tbody id="custom-numbers" style="<?php if (!$info['custom-numbers']) echo 'display:none'; ?>"> + <tr> + <td style="padding-left:20px"> + Format: + </td> + <td> + <input type="text" name="number_format" value="<?php echo $info['number_format']; ?>"/> + <span class="faded">e.g. <span id="format-example"><?php + if ($info['custom-numbers']) { + if ($info['sequence_id']) + $seq = Sequence::lookup($info['sequence_id']); + if (!isset($seq)) + $seq = new RandomSequence(); + echo $seq->current($info['number_format']); + } ?></span></span> + </td> + </tr> + <tr> +<?php $selected = 'selected="selected"'; ?> + <td style="padding-left:20px"> + Sequence: + </td> + <td> + <select name="sequence_id"> + <option value="0" <?php if ($info['sequence_id'] == 0) echo $selected; + ?>>— Random —</option> +<?php foreach (Sequence::objects() as $s) { ?> + <option value="<?php echo $s->id; ?>" <?php + if ($info['sequence_id'] == $s->id) echo $selected; + ?>><?php echo $s->name; ?></option> +<?php } ?> + </select> + <button class="action-button" onclick="javascript: + $.dialog('ajax.php/sequence/manage', 205); + return false; + "><i class="icon-gear"></i> Manage</button> + </td> + </tr> + </tbody> + <tbody> <tr> <th colspan="2"> <em><strong>Admin Notes</strong>: Internal notes about the help topic.</em> @@ -260,3 +315,18 @@ if ($info['form_id'] == Topic::FORM_USE_PARENT) echo 'selected="selected"'; <input type="button" name="cancel" value="Cancel" onclick='window.location.href="helptopics.php"'> </p> </form> +<script type="text/javascript"> +$(function() { + var request = null, + update_example = function() { + request && request.abort(); + request = $.get('ajax.php/sequence/' + + $('[name=sequence_id] :selected').val(), + {'format': $('[name=number_format]').val()}, + function(data) { $('#format-example').text(data); } + ); + }; + $('[name=sequence_id]').on('change', update_example); + $('[name=number_format]').on('keyup', update_example); +}); +</script> diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php index a7960a02b16552d72bd5377a138ccc049b535d46..793b61b85ad39358768030783c1db4a26b8a926b 100644 --- a/include/staff/settings-tickets.inc.php +++ b/include/staff/settings-tickets.inc.php @@ -17,15 +17,41 @@ if(!($maxfileuploads=ini_get('max_file_uploads'))) </tr> </thead> <tbody> - <tr><td width="220" class="required">Ticket IDs:</td> + <tr> + <td> + Default Ticket Number Format: + </td> <td> - <input type="radio" name="random_ticket_ids" value="0" <?php echo !$config['random_ticket_ids']?'checked="checked"':''; ?> /> - Sequential - <input type="radio" name="random_ticket_ids" value="1" <?php echo $config['random_ticket_ids']?'checked="checked"':''; ?> /> - Random + <input type="text" name="number_format" value="<?php echo $config['number_format']; ?>"/> + <span class="faded">e.g. <span id="format-example"><?php + if ($config['sequence_id']) + $seq = Sequence::lookup($config['sequence_id']); + if (!isset($seq)) + $seq = new RandomSequence(); + echo $seq->current($config['number_format']); + ?></span></span> + <i class="help-tip icon-question-sign" href="#number_format"></i> + </td> + </tr> + <tr><td width="220">Default Ticket Number Sequence:</td> +<?php $selected = 'selected="selected"'; ?> + <td> + <select name="sequence_id"> + <option value="0" <?php if ($config['sequence_id'] == 0) echo $selected; + ?>>— Random —</option> +<?php foreach (Sequence::objects() as $s) { ?> + <option value="<?php echo $s->id; ?>" <?php + if ($config['sequence_id'] == $s->id) echo $selected; + ?>><?php echo $s->name; ?></option> +<?php } ?> + </select> + <button class="action-button" onclick="javascript: + $.dialog('ajax.php/sequence/manage', 205); + return false; + "><i class="icon-gear"></i> Manage</button> + <i class="help-tip icon-question-sign" href="#sequence_id"></i> </td> </tr> - <tr> <td width="180" class="required"> Default SLA: @@ -283,4 +309,18 @@ if(!($maxfileuploads=ini_get('max_file_uploads'))) <input class="button" type="reset" name="reset" value="Reset Changes"> </p> </form> - +<script type="text/javascript"> +$(function() { + var request = null, + update_example = function() { + request && request.abort(); + request = $.get('ajax.php/sequence/' + + $('[name=sequence_id] :selected').val(), + {'format': $('[name=number_format]').val()}, + function(data) { $('#format-example').text(data); } + ); + }; + $('[name=sequence_id]').on('change', update_example); + $('[name=number_format]').on('keyup', update_example); +}); +</script> diff --git a/include/staff/templates/sequence-manage.tmpl.php b/include/staff/templates/sequence-manage.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..6c34c9dd99b63af75b800f3d8d6d3613f64d5d10 --- /dev/null +++ b/include/staff/templates/sequence-manage.tmpl.php @@ -0,0 +1,147 @@ +<h3><i class="icon-wrench"></i> Manage Sequences</i></h3> +<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b> +<hr/> +Sequences are used to generate sequential numbers. Various sequences can be +used to generate sequences for different purposes. +<br/> +<br/> +<form method="post" action="<?php echo $info['action']; ?>"> +<div id="sequences"> +<?php +$current_list = array(); +foreach ($sequences as $e) { + $field = function($field, $name=false) use ($e) { ?> + <input class="f<?php echo $field; ?>" type="hidden" name="seq[<?php echo $e->id; + ?>][<?php echo $name ?: $field; ?>]" value="<?php echo $e->{$field}; ?>"/> +<?php }; ?> + <div class="row-item"> + <?php echo $field('name'); echo $field('current', 'next'); echo $field('increment'); echo $field('padding'); ?> + <input type="hidden" class="fdeleted" name="seq[<?php echo $e->get('id'); ?>][deleted]" value="0"/> + <i class="icon-sort-by-order"></i> + <div style="display:inline-block" class="name"> <?php echo $e->getName(); ?> </div> + <div style="display:inline-block;margin-right:60px" class="pull-right"> + <span class="faded">next</span> + <span class="current"><?php echo $e->current(); ?></span> + </div> + <div class="button-group"> + <div class="manage"><a href="#"><i class="icon-cog"></i></a></div> + <div class="delete"><?php if (!$e->hasFlag(Sequence::FLAG_INTERNAL)) { ?> + <a href="#"><i class="icon-trash"></i></a><?php } ?></div> + </div> + <div class="management hidden" data-id="<?php echo $e->id; ?>"> + <table width="100%"><tbody> + <tr><td><label style="padding:0">Increment: + <input class="-increment" type="text" size="4" value="<?php echo Format::htmlchars($e->increment); ?>"/> + </label></td> + <td><label style="padding:0">Padding Character: + <input class="-padding" maxlength="1" type="text" size="4" value="<?php echo Format::htmlchars($e->padding); ?>"/> + </label></td></tr> + </tbody></table> + </div> + </div> +<?php } ?> +</div> + +<div class="row-item hidden" id="template"> + <i class="icon-sort-by-order"></i> + <div style="display:inline-block" class="name"> New Sequence </div> + <div style="display:inline-block;margin-right:60px" class="pull-right"> + <span class="faded">next</span> + <span class="next">1</span> + </div> + <div class="button-group"> + <div class="manage"><a href="#"><i class="icon-cog"></i></a></div> + <div class="delete new"><a href="#"><i class="icon-trash"></i></a></div> + </div> + <div class="management hidden" data-id="<?php echo $e->id; ?>"> + <table width="100%"><tbody> + <tr><td><label style="padding:0">Increment: + <input class="-increment" type="text" size="4" value="1"/> + </label></td> + <td><label style="padding:0">Padding: + <input class="-padding" maxlength="1" type="text" size="4" value="0"/> + </label></td></tr> + </tbody></table> + </div> +</div> + +<hr/> +<button onclick="javascript: + var id = ++$.uid, base = 'seq[new-'+id+']'; + var clone = $('.row-item#template').clone() + .appendTo($('#sequences')) + .removeClass('hidden') + .append($('<input>').attr({type:'hidden',class:'fname',name:base+'[name]',value:'New Sequence'})) + .append($('<input>').attr({type:'hidden',class:'fcurrent',name:base+'[current]',value:'1'})) + .append($('<input>').attr({type:'hidden',class:'fincrement',name:base+'[increment]',value:'1'})) + .append($('<input>').attr({type:'hidden',class:'fpadding',name:base+'[padding]',value:'0'})) ; + clone.find('.manage a').trigger('click'); + return false; +"><i class="icon-plus"></i> Add New Sequence</button> +<div id="delete-warning" style="display:none"> +<hr> + <div id="msg_warning"> + Clicking <strong>Save Changes</strong> will permanently remove the + deleted sequences. + </div> +</div> +<hr> +<div> + <span class="buttons pull-right"> + <input type="submit" value="Save Changes" onclick="javascript: +$('#sequences .save a').each(function() { $(this).trigger('click'); }); +"> + </span> +</div> + +<script type="text/javascript"> +$(function() { + var remove = function() { + if (!$(this).parent().hasClass('new')) { + $('#delete-warning').show(); + $(this).closest('.row-item').hide() + .find('input.fdeleted').val('1'); + } + else + $(this).closest('.row-item').remove(); + return false; + }, manage = function() { + var top = $(this).closest('.row-item'); + top.find('.management').show(200); + top.find('.name').empty().append($('<input class="-name" type="text" size="40">') + .val(top.find('input.fname').val()) + ); + top.find('.current').empty().append($('<input class="-current" type="text" size="10">') + .val(top.find('input.fcurrent').val()) + ); + $(this).find('i').attr('class','icon-save'); + $(this).parent().attr('class','save'); + return false; + }, save = function() { + var top = $(this).closest('.row-item'); + top.find('.management').hide(200); + $.each(['name', 'current'], function(i, t) { + var val = top.find('input.-'+t).val(); + top.find('.'+t).empty().text(val); + top.find('input.f'+t).val(val); + }); + $.each(['increment', 'padding'], function(i, t) { + top.find('input.f'+t).val(top.find('input.-'+t).val()); + }); + $(this).find('i').attr('class','icon-cog'); + $(this).parent().attr('class','manage'); + return false; + }; + $(document).on('click.seq', '#sequences .manage a', manage); + $(document).on('click.seq', '#sequences .save a', save); + $(document).on('click.seq', '#sequences .delete a', remove); + $('.close, input:submit').click(function() { + $(document).die('click.seq'); + /* + $('#sequences .manage a').die(); + $('#sequences .save a').die(); + $('#sequences .delete a').die(); + */ + }); +}); +</script> diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index ce2728dd2adc70580a663146a02de4aa8e476920..797d7f22233c3b37616300c14f5c1e7607941f27 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -408,7 +408,7 @@ if ($results) { <input class="ckb" type="checkbox" name="tids[]" value="<?php echo $row['ticket_id']; ?>" <?php echo $sel?'checked="checked"':''; ?>> </td> <?php } ?> - <td align="center" title="<?php echo $row['email']; ?>" nowrap> + <td title="<?php echo $row['email']; ?>" nowrap> <a class="Icon <?php echo strtolower($row['source']); ?>Ticket ticketPreview" title="Preview Ticket" href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $tid; ?></a></td> <td align="center" nowrap><?php echo Format::db_datetime($row['effective_date']); ?></td> diff --git a/scp/ajax.php b/scp/ajax.php index a192cce8cf92da76bf185a441840093133d03099..d97790120ce8c76b6e87491aabdeb7ba063ff1c9 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -158,6 +158,11 @@ $dispatcher = patterns('', url_delete('^(?P<id>\d+)$', 'deleteNote'), url_post('^attach/(?P<ext_id>\w\d+)$', 'createNote') )), + url('^/sequence/', patterns('ajax.sequence.php:SequenceAjaxAPI', + url_get('^(?P<id>\d+)$', 'current'), + url_get('^manage$', 'manage'), + url_post('^manage$', 'manage') + )), url_post('^/upgrader', array('ajax.upgrader.php:UpgraderAjaxAPI', 'upgrade')), url('^/help/', patterns('ajax.tips.php:HelpTipAjaxAPI', url_get('^tips/(?P<namespace>[\w_.]+)$', 'getTipsJson'), diff --git a/scp/css/scp.css b/scp/css/scp.css index e99d9c36bce30f4c7e0b57f83e410dc0efc2ddf8..95d63bbf0efef64e7371bf25b9013dfdab2e43fe 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -1646,43 +1646,58 @@ div.selected-signature .inner { cursor: move; } -.sortable-row-item { +.sortable { + cursor: move; +} +.row-item { border: 1px solid rgba(0, 0, 0, 0.7); padding: 9px; - cursor: move; position: relative; } -.sortable-row-item:hover { +.sortable:hover { background: rgba(0, 0, 0, 0.1); } -.sortable-row-item:active { +.sortable:active { background: rgba(0, 0, 0, 0.3); } -.sortable-row-item:first-child { +.row-item:first-child { border-top-right-radius: 5px; border-top-left-radius: 5px; } -.sortable-row-item:last-child { +.row-item:last-child { border-bottom-right-radius: 5px; border-bottom-left-radius: 5px; } -.sortable-row-item + .sortable-row-item { +.row-item + .row-item { margin-top: -1px; } -.sortable-row-item .delete { +.row-item .delete { border-left: 1px solid rgba(0, 0, 0, 0.7); border-top-right-radius: inherit; border-bottom-right-radius: inherit; + width: 35px; +} + +.row-item .button-group { + font-size: 105%; position: absolute; top: 0px; right: 0; + display: inline-block; +} + +.row-item .button-group div { padding: 9px; padding-left: 12px; - font-size: 105%; + display: inline-block; +} +.row-item .management { + margin-top: 10px; + border-top: 1px dashed black; } -.sortable-row-item .delete:hover { +.row-item .delete:hover { background: #fc9f41; /* Old browsers */ color: rgba(255,255,255,0.8) !important; } @@ -1839,3 +1854,7 @@ table.custom-info td { .delete-draft:hover { background-color: #fc9f41 !important; } + +.hidden { + display: none; +} diff --git a/scp/js/scp.js b/scp/js/scp.js index d79b03955150d2221fe8a3ef920c1a021db5b301..a57503809f98786e275e132923af1cf201ae6abe 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -578,6 +578,8 @@ $.orgLookup = function (url, cb) { }); }; +$.uid = 1; + //Tabs $(document).on('click.tab', 'ul.tabs li a', function(e) { e.preventDefault();