diff --git a/include/class.i18n.php b/include/class.i18n.php index 884dc7cb9863f51e796e5fe812c03eeb67d697ab..a5d9e7c7b1ab36672dcb944da414b89224310a21 100644 --- a/include/class.i18n.php +++ b/include/class.i18n.php @@ -57,6 +57,8 @@ class Internationalization { 'team.yaml' => 'Team::create', // Organization 'organization.yaml' => 'Organization::__create', + // Ticket + 'ticket_status.yaml' => 'TicketStatus::__create', // Note that group requires department 'group.yaml' => 'Group::create', 'file.yaml' => 'AttachmentFile::create', diff --git a/include/class.list.php b/include/class.list.php index 2ba87622560907893962a6e62656310df4c21860..293165bbd285920408e9882f43cd687945370aa6 100644 --- a/include/class.list.php +++ b/include/class.list.php @@ -618,23 +618,52 @@ class TicketStatusList extends BuiltInCustomList { } function getNumItems() { - return 0; + return TicketStatus::objects()->count(); } function getAllItems() { - + return TicketStatus::objects()->order_by($this->getListOrderBy()); } function getItems($criteria) { + if (!$this->_items) { + $this->_items = TicketStatus::objects()->filter( + array('flags__hasbit' => TicketStatus::ENABLED)) + ->order_by($this->getListOrderBy()); + if ($criteria['limit']) + $this->_items->limit($criteria['limit']); + if ($criteria['offset']) + $this->_items->offset($criteria['offset']); + } + + return $this->_items; } + function getItem($id) { + return TicketStatus::lookup($id); + } + + function addItem($vars, &$errors) { + $item = TicketStatus::create(array( + 'flags' => 0, //Disable until configured. + 'sort' => $vars['sort'], + 'name' => $vars['value'], + )); + $item->save(); + + $this->_items = false; + + return $item; + } + + function hasProperties() { - return true; + return ($this->getForm()); } function getListOrderBy() { - switch ($this->setSortMode()) { + switch ($this->getSortMode()) { case 'Alpha': return 'name'; case '-Alpha': @@ -645,20 +674,304 @@ class TicketStatusList extends BuiltInCustomList { } } - function getForm() { - return null; - } - function update($vars, &$errors) { foreach (static::$config_fields as $f) { if (!isset($vars[$f])) continue; if (parent::set($f, $vars[$f])) - $this->ht[$field] = $vars[$f]; + $this->ht[$f] = $vars[$f]; } return true; } + + function getForm() { + + if (!isset($this->_form)) { + $o = DynamicForm::objects()->filter(array('type'=>'S')); + if ($o && $o[0]) + $this->_form = $o[0]; + else // Auto-load the data + $this->_form = self::__load(); + } + + return $this->_form; + } + + + static function __load() { + require_once(INCLUDE_DIR.'class.i18n.php'); + + $i18n = new Internationalization(); + $tpl = $i18n->getTemplate('form.yaml'); + foreach ($tpl->getData() as $f) { + if ($f['type'] == 'S') { + $form = DynamicForm::create($f); + $form->save(); + break; + } + } + + $o = DynamicForm::objects()->filter(array('type'=>'S')); + if (!$form || !$o) + return false; + + // Create default statuses + if (($statuses = $i18n->getTemplate('ticket_status.yaml')->getData())) + foreach ($statuses as $status) + TicketStatus::__create($status); + + return $o[0]; + } } + +class TicketStatus extends VerySimpleModel implements CustomListItem { + + static $meta = array( + 'table' => TICKET_STATUS_TABLE, + 'ordering' => array('name'), + 'pk' => array('id'), + ); + + // Major statuses (states) + static $_states = array( 1 => 'open', 'closed', 'archived', 'deleted'); + + // Supported flags (TODO: move to configurable custom list) + static $_flags = array( + 'onhold' => array( + 'flag' => 1, + 'name' => 'Onhold', + 'states' => array('open'), + ), + 'overdue' => array( + 'flag' => 2, + 'name' => 'Overdue', + 'states' => array('open'), + ), + 'answered' => array( + 'flag' => 4, + 'name' => 'Answered', + 'states' => array('open'), + ) + ); + + var $_form; + var $_config; + var $_settings; + + + const ENABLED = 0x0001; + const INTERNAL = 0x0002; // Forbid deletion or name and status change. + + + + function __construct() { + call_user_func_array(array('parent', '__construct'), func_get_args()); + $this->_config = new Config('TS.'.$this->getId()); + } + + protected function hasFlag($field, $flag) { + return 0 !== ($this->get($field) & $flag); + } + + protected function clearFlag($field, $flag) { + return $this->set($field, $this->get($field) & ~$flag); + } + + protected function setFlag($field, $flag) { + return $this->set($field, $this->get($field) | $flag); + } + + function getForm() { + return $this->getConfigurationForm(); + } + + function getConfigurationForm() { + + if (!$this->_form) { + $this->_form = DynamicForm::lookup(array('type'=>'S')); + } + + return $this->_form; + } + + function isEnabled() { + return $this->hasFlag('mode', self::ENABLED); + } + + function enable() { + + // Ticket status without properties cannot be enabled! + if (!$this->isEnableable()) + return false; + + return $this->setFlag('mode', self::ENABLED); + } + + function disable() { + return (!$this->isInternal() + && $this->clearFlag('mode', self::ENABLED)); + } + + function isEnableable() { + return ($this->getForm()); + } + + function isDeletable() { + return !$this->isInternal(); + } + + function isInternal() { + return ($this->hasFlag('mode', self::INTERNAL)); + } + + function getId() { + return $this->get('id'); + } + + function getName() { + return $this->get('name'); + } + + function getValue() { + return $this->getName(); + } + + function getAbbrev() { + return ''; + } + + function getSortOrder() { + return $this->get('sort'); + } + + function getConfiguration() { + + if (!$this->_settings) { + $this->_settings = $this->_config->get('properties'); + if (is_string($this->_settings)) + $this->_settings = JsonDataParser::parse($this->_settings); + elseif (!$this->_settings) + $this->_settings = array(); + + if ($this->getConfigurationForm()) { + foreach ($this->getConfigurationForm()->getFields() as $f) { + $name = mb_strtolower($f->get('name')); + $id = $f->get('id'); + switch($name) { + case 'flags': + foreach (static::$_flags as $k => $v) + if ($this->hasFlag('flags', $v['flag'])) + $this->_settings[$id][] = $k; + break; + case 'state': + $this->_settings[$id] = $this->get('state'); + break; + default: + if (!$this->_settings[$id] && $this->_settings[$name]) + $this->_settings[$id] = $this->_settings[$name]; + } + } + } + } + + return $this->_settings; + } + + function setConfiguration(&$errors=array()) { + $properties = array(); + foreach ($this->getConfigurationForm()->getFields() as $f) { + $val = $f->to_database($f->getClean()); + $name = mb_strtolower($f->get('name')); + switch ($name) { + case 'flags': + $val = $f->getClean(); + if ($val && is_array($val)) { + $flags = 0; + foreach ($val as $v) { + if (isset(static::$_flags[$v])) + $flags += static::$_flags[$v]['flag']; + elseif (!$f->errors()) + $f->addError('Unknown or invalid flag', $name); + } + $this->set('flags', $flags); + } elseif ($val && !$f->errors()) { + $f->addError('Unknown or invalid flag format', $name); + } + break; + case 'state': + if ($val && in_array($val, static::$_states)) + $this->set('state', $val); + else + $f->addError('Unknown or invalid state', $name); + break; + default: //Custom properties the user might add. + $properties[$f->get('id')] = $val; + } + $errors = array_merge($errors, $f->errors()); + } + + if (count($errors) === 0) { + $this->save(true); + $this->setProperties($properties); + } + + return count($errors) === 0; + } + + function setProperties($properties) { + if ($properties && is_array($properties)) + $properties = JsonDataEncoder::encode($properties); + + $this->_config->set('properties', $properties); + } + + function update($vars, &$errors) { + + $fields = array('value' => 'name', 'sort' => 'sort'); + foreach($fields as $k => $v) { + if (isset($vars[$k])) + $this->set($v, $vars[$k]); + } + + return $this->save(true); + } + + function delete() { + + if (!$this->isDeletable()) + return false; + + // TODO: Delete and do house cleaning (move tickets..etc) + + } + + function toString() { + return $this->getValue(); + } + + function __toString() { + return $this->toString(); + } + + static function __create($ht, &$error=false) { + global $ost; + + $properties = JsonDataEncoder::encode($ht['properties']); + unset($ht['properties']); + $ht['created'] = new SqlFunction('NOW'); + if ($status = TicketStatus::create($ht)) { + $status->save(true); + $status->_config = new Config('TS.'.$status->getId()); + $status->_config->set('properties', $properties); + } + + return $status; + } +} + + + + ?> diff --git a/include/i18n/en_US/form.yaml b/include/i18n/en_US/form.yaml index fe4be2f66098d0c8de9daee25c785ed3d04260f9..cefe3a072e12747748bb601540b6a7aefac0a71b 100644 --- a/include/i18n/en_US/form.yaml +++ b/include/i18n/en_US/form.yaml @@ -198,3 +198,46 @@ configuration: rows: 4 cols: 40 + +- type: S # notrans + title: Ticket Status Properties + instructions: Properties that can be set on a ticket status. + deletable: false + fields: + - type: choices # notrans + name: state # notrans + label: State + required: true + sort: 1 + edit_mask: 63 + configuration: + choices:| + open: Open + closed: Closed + archived: Archived + deleted: Deleted + prompt: State of a ticket + - type: choices # notrans + name: flags # notrans + label: Flags + required: false + sort: 2 + edit_mask: 63 + configuration: + choices:| + onhold: Onhold (Disable SLA) + overdue: Mark as Overdue + answered: Mark as Answered + prompt: Status flags + multiselect: true + - type: memo # notrans + name: description + label: Description + required: false + sort: 3 + edit_mask: 15 + configuration: + rows: 2 + cols: 40 + html: false + length: 100 diff --git a/include/i18n/en_US/ticket_status.yaml b/include/i18n/en_US/ticket_status.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3c41d326f8a5e4aa8151f62f1dc89a2ae806bcb6 --- /dev/null +++ b/include/i18n/en_US/ticket_status.yaml @@ -0,0 +1,64 @@ +# +# Default system data for ticket statuses +# +# Fields: +# id - (int:optional) id number in the database +# name - (string) descriptive name of the status +# state - (string) Main status of a ticket +# (open, resolved, closed, archived, deleted) +# mode - (bit) access mask (1 - enabled, 2 - internal) +# flags - (bit) flags that can be set on a ticket +# properties: +# description - (string) Description of the status +# +--- +- id: 1 + name: Open + state: open + mode: 3 + sort: 1 + flags: 0 + properties: + description: > + Open tickets. + +- id: 2 + name: Resolved + state: closed + mode: 3 + sort: 2 + flags: 0 + properties: + description: > + Resolved tickets are closed tickets that can be reopened by the end user. This might be useful + when a trigger is used to close resolved tickets with notice sent to end user. + +- id: 3 + name: Closed + state: closed + mode: 3 + sort: 3 + flags: 0 + properties: + description: > + Tickets marked as closed cannot be reopened by the end user. Tickets will still be accessible on client and staff panels. + +- id: 4 + name: Archived + state: archived + mode: 3 + sort: 4 + flags: 0 + properties: + description: > + Tickets only adminstratively available but nolonger accessible on ticket queues. + +- id: 5 + name: Deleted + state: deleted + mode: 3 + sort: 5 + flags: 0 + properties: + description: > + Tickets queued for deletion. Not accessible on ticket queues. diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig index 324095cf42b6af08769dcb485913dacba7cc301d..f3561a24cf47f64ff653740c9d2de13c3cadcd56 100644 --- a/include/upgrader/streams/core.sig +++ b/include/upgrader/streams/core.sig @@ -1 +1 @@ -8f99b8bf9bee63c8e4dc274ffbdda383 +9b9e2dc4551d448f081f180ca3829fa8 diff --git a/include/upgrader/streams/core/8f99b8bf-9b9e2dc4.patch.sql b/include/upgrader/streams/core/8f99b8bf-9b9e2dc4.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..b08c0a972266e4aa677d38abf5b2d30fae8d7ba4 --- /dev/null +++ b/include/upgrader/streams/core/8f99b8bf-9b9e2dc4.patch.sql @@ -0,0 +1,24 @@ +/** + * @version v1.9.3 + * @signature 9b9e2dc4551d448f081f180ca3829fa8 + * @title Add custom ticket status support + * + */ + +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%ticket_status` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(60) NOT NULL DEFAULT '', + `state` varchar(16) NOT NULL DEFAULT 'open', + `mode` int(11) unsigned NOT NULL DEFAULT '0', + `flags` int(10) unsigned NOT NULL DEFAULT '0', + `sort` int(11) unsigned NOT NULL DEFAULT '0', + `notes` text NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) DEFAULT CHARSET=utf8; + +UPDATE `%TABLE_PREFIX%config` + SET `value` = '9b9e2dc4551d448f081f180ca3829fa8' + WHERE `key` = 'schema_signature' AND `namespace` = 'core'; diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index 20c3dc3460c7cb3f323d1f6425e0844d9b6ffa3d..a7c27d76aae8ca172d5d96e160be933aff64d2da 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -650,6 +650,22 @@ CREATE TABLE `%TABLE_PREFIX%ticket_event` ( KEY `ticket_stats` (`timestamp`, `state`) ) DEFAULT CHARSET=utf8; +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_status`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%ticket_status` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(60) NOT NULL DEFAULT '', + `state` varchar(16) NOT NULL DEFAULT 'open', + `mode` int(11) unsigned NOT NULL DEFAULT '0', + `flags` int(10) unsigned NOT NULL DEFAULT '0', + `sort` int(11) unsigned NOT NULL DEFAULT '0', + `notes` text NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) DEFAULT CHARSET=utf8; + + DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_priority`; CREATE TABLE `%TABLE_PREFIX%ticket_priority` ( `priority_id` tinyint(4) NOT NULL auto_increment,