diff --git a/attachment.php b/attachment.php index 05e256a42a980c0c15e5469167af2a047952718c..8e45d8e155c83cae6c31697f49f5b8bfd3910a9e 100644 --- a/attachment.php +++ b/attachment.php @@ -16,21 +16,23 @@ **********************************************************************/ require('secure.inc.php'); require_once(INCLUDE_DIR.'class.attachment.php'); -//Basic checks -if(!$thisclient +// Basic checks +if (!$thisclient || !$_GET['id'] || !$_GET['h'] || !($attachment=Attachment::lookup($_GET['id'])) - || !($file=$attachment->getFile())) + || !($file=$attachment->getFile()) + || strcasecmp(trim($_GET['h']), $file->getDownloadHash()) + || !($object=$attachment->getObject()) + || !$object instanceof ThreadEntry + || !($ticket=$object->getThread()->getObject()) + || !$ticket instanceof Ticket + ) Http::response(404, __('Unknown or invalid file')); -//Validate session access hash - we want to make sure the link is FRESH! and the user has access to the parent ticket!! -$vhash=md5($attachment->getFileId().session_id().strtolower($file->getKey())); -if(strcasecmp(trim($_GET['h']),$vhash) - || !($ticket=$attachment->getTicket()) - || !$ticket->checkUserAccess($thisclient)) - Http::response(404, __('Unknown or invalid file')); -//Download the file.. -$file->download(); +if (!$ticket->checkUserAccess($thisclient)) + die(__('Access Denied')); +// Download the file.. +$file->download(); ?> diff --git a/bootstrap.php b/bootstrap.php index 424f3637c1bc5cc9ecde66edd30fd07555490562..d2afa2243f7bfa2914da15428c3f8e2c9b197fcb 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -89,16 +89,20 @@ class Bootstrap { define('FAQ_CATEGORY_TABLE',$prefix.'faq_category'); define('DRAFT_TABLE',$prefix.'draft'); + + define('THREAD_TABLE', $prefix.'thread'); + define('THREAD_ENTRY_TABLE', $prefix.'thread_entry'); + define('THREAD_ENTRY_EMAIL_TABLE', $prefix.'thread_entry_email'); + define('TICKET_TABLE',$prefix.'ticket'); - define('TICKET_THREAD_TABLE',$prefix.'ticket_thread'); - define('TICKET_ATTACHMENT_TABLE',$prefix.'ticket_attachment'); define('TICKET_LOCK_TABLE',$prefix.'ticket_lock'); define('TICKET_EVENT_TABLE',$prefix.'ticket_event'); - define('TICKET_EMAIL_INFO_TABLE',$prefix.'ticket_email_info'); define('TICKET_COLLABORATOR_TABLE', $prefix.'ticket_collaborator'); define('TICKET_STATUS_TABLE', $prefix.'ticket_status'); define('TICKET_PRIORITY_TABLE',$prefix.'ticket_priority'); + define('TASK_TABLE',$prefix.'task'); + define('PRIORITY_TABLE',TICKET_PRIORITY_TABLE); @@ -184,6 +188,7 @@ class Bootstrap { require_once INCLUDE_DIR.'class.util.php'; require_once INCLUDE_DIR.'class.translation.php'; require(INCLUDE_DIR.'class.signal.php'); + require(INCLUDE_DIR.'class.model.php'); require(INCLUDE_DIR.'class.user.php'); require(INCLUDE_DIR.'class.auth.php'); require(INCLUDE_DIR.'class.pagenate.php'); //Pagenate helper! diff --git a/include/ajax.draft.php b/include/ajax.draft.php index 8d264ff486639b94df5622028ea611c656f130a0..2687ad94b53fb28e074d856394489aeae2c435db 100644 --- a/include/ajax.draft.php +++ b/include/ajax.draft.php @@ -358,7 +358,7 @@ class DraftAjaxAPI extends AjaxController { } } $field_list = array('response', 'note', 'answer', 'body', - 'message', 'issue'); + 'message', 'issue', 'description'); foreach ($field_list as $field) { if (isset($vars[$field])) { return urldecode($vars[$field]); diff --git a/include/ajax.orgs.php b/include/ajax.orgs.php index b8309a55edae5ecfadf028e155b7ca45bc27bac4..aa6937bc38f9c96312ee53be6da45737b795c4e6 100644 --- a/include/ajax.orgs.php +++ b/include/ajax.orgs.php @@ -255,7 +255,7 @@ class OrgsAjaxAPI extends AjaxController { } function manageForms($org_id) { - $forms = DynamicFormEntry::forOrganization($org_id); + $forms = DynamicFormEntry::forObject($org_id, 'O'); $info = array('action' => '#orgs/'.Format::htmlchars($org_id).'/forms/manage'); include(STAFFINC_DIR . 'templates/form-manage.tmpl.php'); } @@ -271,7 +271,7 @@ class OrgsAjaxAPI extends AjaxController { Http::response(422, "Send updated forms list"); // Add new forms - $forms = DynamicFormEntry::forOrganization($org_id); + $forms = DynamicFormEntry::forObject($org_id, 'O'); foreach ($_POST['forms'] as $sort => $id) { $found = false; foreach ($forms as $e) { diff --git a/include/ajax.reports.php b/include/ajax.reports.php index bdaa7d9fe3fb828fe7428ed7a2c4cb03101d328d..d432fee88ba7166aff44dba6e0385d89adfedb51 100644 --- a/include/ajax.reports.php +++ b/include/ajax.reports.php @@ -111,9 +111,11 @@ class OverviewReportAjaxAPI extends AjaxController { FORMAT(AVG(DATEDIFF(B2.created, B1.created)),1) AS ResponseTime FROM '.$info['table'].' T1 LEFT JOIN '.TICKET_TABLE.' T2 ON (T2.'.$info['pk'].'=T1.'.$info['pk'].') - LEFT JOIN '.TICKET_THREAD_TABLE.' B2 ON (B2.ticket_id = T2.ticket_id - AND B2.thread_type="R") - LEFT JOIN '.TICKET_THREAD_TABLE.' B1 ON (B2.pid = B1.id) + LEFT JOIN '.THREAD_TABLE.' B0 ON (B0.object_id=T2.ticket_id + AND B0.object_type="T") + LEFT JOIN '.THREAD_ENTRY_TABLE.' B2 ON (B2.thread_id = B0.id + AND B2.`type`="R") + LEFT JOIN '.THREAD_ENTRY_TABLE.' B1 ON (B2.pid = B1.id) LEFT JOIN '.STAFF_TABLE.' S1 ON (S1.staff_id=B2.staff_id) WHERE '.$info['filter'].' AND B1.created BETWEEN '.$start.' AND '.$stop.' GROUP BY T1.'.$info['pk'].' diff --git a/include/ajax.tasks.php b/include/ajax.tasks.php new file mode 100644 index 0000000000000000000000000000000000000000..ab8bce914e6b87629fc43d12ff14deaaaf137cc5 --- /dev/null +++ b/include/ajax.tasks.php @@ -0,0 +1,169 @@ +<?php +/********************************************************************* + ajax.tasks.php + + AJAX interface for tasks + + Peter Rotich <peter@osticket.com> + Copyright (c) 20014 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +if(!defined('INCLUDE_DIR')) die('403'); + +include_once(INCLUDE_DIR.'class.ticket.php'); +require_once(INCLUDE_DIR.'class.ajax.php'); +require_once(INCLUDE_DIR.'class.task.php'); + +class TasksAjaxAPI extends AjaxController { + + function preview($tid) { + global $thisstaff; + + // TODO: check staff's access. + if(!$thisstaff || !($task=Task::lookup($tid))) + Http::response(404, __('No such task')); + + include STAFFINC_DIR . 'templates/task-preview.tmpl.php'; + } + + function edit($tid) { + global $thisstaff; + + // TODO: check staff's access. + if(!$thisstaff || !($task=Task::lookup($tid))) + Http::response(404, __('No such task')); + + $info = $errors = array(); + $forms = DynamicFormEntry::forObject($task->getId(), + ObjectModel::OBJECT_TYPE_TASK); + + if ($_POST) { + $info = Format::htmlchars($_POST); + $info['error'] = $errors['err'] ?: __('Coming soon!'); + } + + include STAFFINC_DIR . 'templates/task-edit.tmpl.php'; + } + + function transfer($tid) { + global $thisstaff; + + // TODO: check staff's access. + if(!$thisstaff || !($task=Task::lookup($tid))) + Http::response(404, __('No such task')); + + $info = $errors = array(); + if ($_POST) { + if ($task->transfer($_POST, $errors)) { + Http::response(201, $task->getId()); + + } + + $info = Format::htmlchars($_POST); + $info['error'] = $errors['err'] ?: __('Unable to transfer task'); + } + + include STAFFINC_DIR . 'templates/task-transfer.tmpl.php'; + } + + function assign($tid) { + global $thisstaff; + + // TODO: check staff's access. + if(!$thisstaff || !($task=Task::lookup($tid))) + Http::response(404, __('No such task')); + + $info = $errors = array(); + if ($_POST) { + if ($task->assign($_POST, $errors)) { + Http::response(201, $task->getId()); + + } + + $info = Format::htmlchars($_POST); + $info['error'] = $errors['err'] ?: __('Unable to assign task'); + } + + include STAFFINC_DIR . 'templates/task-assign.tmpl.php'; + } + + function delete($tid) { + global $thisstaff; + + // TODO: check staff's access. + if(!$thisstaff || !($task=Task::lookup($tid))) + Http::response(404, __('No such task')); + + $info = $errors = array(); + if ($_POST) { + if ($task->delete($_POST, $errors)) { + Http::response(201, 0); + + } + + $info = Format::htmlchars($_POST); + $info['error'] = $errors['err'] ?: __('Unable to delete task'); + } + $info['placeholder'] = sprintf(__( + 'Optional reason for deleting %s'), + __('this task')); + $info['warn'] = sprintf(__( + 'Are you sure you want to DELETE %s?'), + __('this task')); + $info['extra'] = sprintf('<strong>%s</strong>', + __('Deleted tasks CANNOT be recovered, including any associated attachments.') + ); + + include STAFFINC_DIR . 'templates/task-delete.tmpl.php'; + } + + + function task($tid) { + global $thisstaff; + + // TODO: check staff's access. + if (!$thisstaff || !($task=Task::lookup($tid))) + Http::response(404, __('No such task')); + + $info=$errors=array(); + $task_note_form = new Form(array( + 'attachments' => new FileUploadField(array('id'=>'attach', + 'name'=>'attach:note', + 'configuration' => array('extensions'=>''))) + )); + + if ($_POST) { + + switch ($_POST['a']) { + case 'postnote': + $vars = $_POST; + $attachments = $task_note_form->getField('attachments')->getClean(); + $vars['cannedattachments'] = array_merge( + $vars['cannedattachments'] ?: array(), $attachments); + if(($note=$task->postNote($vars, $errors, $thisstaff))) { + $msg=__('Note posted successfully'); + // Clear attachment list + $task_note_form->setSource(array()); + $task_note_form->getField('attachments')->reset(); + Draft::deleteForNamespace('task.note.'.$task->getId(), + $thisstaff->getId()); + } else { + if(!$errors['err']) + $errors['err'] = __('Unable to post the note - missing or invalid data.'); + } + break; + default: + $errors['err'] = __('Unknown action'); + } + } + + include STAFFINC_DIR . 'templates/task-view.tmpl.php'; + } +} +?> diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 196bcc390e7147479bc5fd073dfaef3ec25e331b..6cd63e98ad71c3603754b88e25cc78ee61d1bf4b 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -854,5 +854,69 @@ class TicketsAjaxAPI extends AjaxController { include(STAFFINC_DIR . 'templates/ticket-status.tmpl.php'); } + + function tasks($tid) { + global $thisstaff; + + if (!($ticket=Ticket::lookup($tid)) + || !$ticket->checkStaffAccess($thisstaff)) + Http::response(404, 'Unknown ticket'); + + include STAFFINC_DIR . 'ticket-tasks.inc.php'; + } + + function addTask($tid) { + global $thisstaff; + + if (!($ticket=Ticket::lookup($tid)) + || !$ticket->checkStaffAccess($thisstaff)) + Http::response(404, 'Unknown ticket'); + + $info=$errors=array(); + + if ($_POST) { + Draft::deleteForNamespace( + sprintf('ticket.%d.task', $ticket->getId()), + $thisstaff->getId()); + // Default form + $form = TaskForm::getDefaultForm()->getForm($_POST); + // Internal form + $iform = TaskForm::getInternalForm($_POST); + $isvalid = true; + if (!$iform->isValid()) + $isvalid = false; + if (!$form->isValid()) + $isvalid = false; + + if ($isvalid) { + $vars = $_POST; + $vars['object_id'] = $ticket->getId(); + $vars['object_type'] = ObjectModel::OBJECT_TYPE_TICKET; + $vars['default_formdata'] = $form->getClean(); + $vars['internal_formdata'] = $iform->getClean(); + $desc = $form->getField('description'); + if ($desc + && $desc->isAttachmentsEnabled() + && ($attachments=$desc->getWidget()->getAttachments())) + $vars['cannedattachments'] = $attachments->getClean(); + $vars['staffId'] = $thisstaff->getId(); + $vars['poster'] = $thisstaff; + $vars['ip_address'] = $_SERVER['REMOTE_ADDR']; + if (($task=Task::create($vars, $errors))) + Http::response(201, $task->getId()); + } + + $info['error'] = __('Error adding task - try again!'); + } + + $info['action'] = sprintf('#tickets/%d/add-task', $ticket->getId()); + $info['title'] = sprintf( + __( 'Ticket #%1$s: %2$s'), + $ticket->getNumber(), + _('Add New Task') + ); + + include STAFFINC_DIR . 'templates/task.tmpl.php'; + } } ?> diff --git a/include/ajax.users.php b/include/ajax.users.php index f211ff6e644238b29576f92313d8695e6f74cc51..02a3b1668427dd011db3b9edba9c6b063e584dd2 100644 --- a/include/ajax.users.php +++ b/include/ajax.users.php @@ -412,7 +412,7 @@ class UsersAjaxAPI extends AjaxController { } function manageForms($user_id) { - $forms = DynamicFormEntry::forUser($user_id); + $forms = DynamicFormEntry::forObject($user_id, 'U'); $info = array('action' => '#users/'.Format::htmlchars($user_id).'/forms/manage'); include(STAFFINC_DIR . 'templates/form-manage.tmpl.php'); } @@ -428,7 +428,7 @@ class UsersAjaxAPI extends AjaxController { Http::response(422, "Send updated forms list"); // Add new forms - $forms = DynamicFormEntry::forUser($user_id); + $forms = DynamicFormEntry::forObject($user_id, 'U'); foreach ($_POST['forms'] as $sort => $id) { $found = false; foreach ($forms as $e) { diff --git a/include/class.api.php b/include/class.api.php index 0fd2c17e3f0ab552513c775e5f2f93ee52bc1e7f..9fe2142044062ef86033447ddf72a009a142bf6f 100644 --- a/include/class.api.php +++ b/include/class.api.php @@ -348,9 +348,9 @@ class ApiXmlDataParser extends XmlDataParser { $value['body'] = Format::utf8encode($value['body'], $value['encoding']); if (!strcasecmp($value['type'], 'text/html')) - $value = new HtmlThreadBody($value['body']); + $value = new HtmlThreadEntryBody($value['body']); else - $value = new TextThreadBody($value['body']); + $value = new TextThreadEntryBody($value['body']); } else if ($key == "attachments") { if(!isset($value['file'][':text'])) @@ -393,9 +393,9 @@ class ApiJsonDataParser extends JsonDataParser { $data = Format::parseRfc2397($value, 'utf-8'); if (isset($data['type']) && $data['type'] == 'text/html') - $value = new HtmlThreadBody($data['data']); + $value = new HtmlThreadEntryBody($data['data']); else - $value = new TextThreadBody($data['data']); + $value = new TextThreadEntryBody($data['data']); } else if ($key == "attachments") { foreach ($value as &$info) { diff --git a/include/class.attachment.php b/include/class.attachment.php index d7f2d7f668cbd920cbc25dee19abf8364b5862ca..b602b451c253587ff519c28419063a42ac5b44b8 100644 --- a/include/class.attachment.php +++ b/include/class.attachment.php @@ -19,48 +19,27 @@ require_once(INCLUDE_DIR.'class.file.php'); class Attachment { var $id; var $file_id; - var $ticket_id; - var $info; + var $ht; + var $object; - function Attachment($id,$tid=0) { + function Attachment($id) { - $sql='SELECT * FROM '.TICKET_ATTACHMENT_TABLE.' WHERE attach_id='.db_input($id); - if($tid) - $sql.=' AND ticket_id='.db_input($tid); + $sql = 'SELECT a.* FROM '.ATTACHMENT_TABLE.' a ' + . 'WHERE a.id='.db_input($id); + if (!($res=db_query($sql)) || !db_num_rows($res)) + return; - if(!($res=db_query($sql)) || !db_num_rows($res)) - return false; - - $this->ht=db_fetch_array($res); - - $this->id=$this->ht['attach_id']; - $this->file_id=$this->ht['file_id']; - $this->ticket_id=$this->ht['ticket_id']; - - $this->file=null; - $this->ticket=null; - - return true; + $this->ht = db_fetch_array($res); + $this->file = $this->object = null; } function getId() { - return $this->id; - } - - function getTicketId() { - return $this->ticket_id; - } - - function getTicket() { - if(!$this->ticket && $this->getTicketId()) - $this->ticket = Ticket::lookup($this->getTicketId()); - - return $this->ticket; + return $this->ht['id']; } function getFileId() { - return $this->file_id; + return $this->ht['file_id']; } function getFile() { @@ -70,10 +49,6 @@ class Attachment { return $this->file; } - function getCreateDate() { - return $this->ht['created']; - } - function getHashtable() { return $this->ht; } @@ -82,25 +57,36 @@ class Attachment { return $this->getHashtable(); } - /* Static functions */ - function getIdByFileHash($hash, $tid=0) { - $sql='SELECT attach_id FROM '.TICKET_ATTACHMENT_TABLE.' a ' + function getObject() { + + if (!isset($this->object)) + $this->object = ObjectModel::lookup( + $this->ht['object_id'], $this->ht['type']); + + return $this->object; + } + + static function getIdByFileHash($hash, $objectId=0) { + $sql='SELECT a.id FROM '.ATTACHMENT_TABLE.' a ' .' INNER JOIN '.FILE_TABLE.' f ON(f.id=a.file_id) ' .' WHERE f.`key`='.db_input($hash); - if($tid) - $sql.=' AND a.ticket_id='.db_input($tid); + if ($objectId) + $sql.=' AND a.object_id='.db_input($objectId); return db_result(db_query($sql)); } - function lookup($var,$tid=0) { - $id=is_numeric($var)?$var:self::getIdByFileHash($var,$tid); + static function lookup($var, $objectId=0) { - return ($id && is_numeric($id) - && ($attach = new Attachment($id,$tid)) - && $attach->getId()==$id)?$attach:null; - } + $id = is_numeric($var) ? $var : self::getIdByFileHash($var, + $objectId); + return ($id + && is_numeric($id) + && ($attach = new Attachment($id, $objectId)) + && $attach->getId()==$id + ) ? $attach : null; + } } class GenericAttachments { @@ -175,7 +161,8 @@ class GenericAttachments { function _getList($separate=false, $inlines=false, $lang=false) { if(!isset($this->attachments)) { $this->attachments = array(); - $sql='SELECT f.id, f.size, f.`key`, f.name, a.inline, a.lang ' + $sql='SELECT f.id, f.size, f.`key`, f.name ' + .', a.inline, a.lang, a.id as attach_id ' .' FROM '.FILE_TABLE.' f ' .' INNER JOIN '.ATTACHMENT_TABLE.' a ON(f.id=a.file_id) ' .' WHERE a.`type`='.db_input($this->getType()) @@ -193,7 +180,7 @@ class GenericAttachments { if (($a['inline'] != $separate || $a['inline'] == $inlines) && $lang == $a['lang']) { $a['file_id'] = $a['id']; - $a['hash'] = md5($a['file_id'].session_id().strtolower($a['key'])); + $a['hash'] = md5($a['file_id'].session_id().$a['key']); $attachments[] = $a; } } diff --git a/include/class.config.php b/include/class.config.php index 6fd4208b64e2f1e9a0f8fbc3aa5d44a8fb161704..ab5d822fab63161344ae796386c8a13172d13b2b 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -655,22 +655,44 @@ class OsticketConfig extends Config { return true; //No longer an option...hint: big plans for headers coming!! } - function getDefaultSequence() { - if ($this->get('sequence_id')) - $sequence = Sequence::lookup($this->get('sequence_id')); + function getDefaultTicketSequence() { + if ($this->get('ticket_sequence_id')) + $sequence = Sequence::lookup($this->get('ticket_sequence_id')); if (!$sequence) $sequence = new RandomSequence(); return $sequence; } - function getDefaultNumberFormat() { - return $this->get('number_format'); + + function getDefaultTicketNumberFormat() { + return $this->get('ticket_number_format'); } + function getNewTicketNumber() { - $s = $this->getDefaultSequence(); - return $s->next($this->getDefaultNumberFormat(), + $s = $this->getDefaultTicketSequence(); + return $s->next($this->getDefaultTicketNumberFormat(), array('Ticket', 'isTicketNumberUnique')); } + // Task sequence + function getDefaultTaskSequence() { + if ($this->get('task_sequence_id')) + $sequence = Sequence::lookup($this->get('task_sequence_id')); + if (!$sequence) + $sequence = new RandomSequence(); + + return $sequence; + } + + function getDefaultTaskNumberFormat() { + return $this->get('task_number_format'); + } + + function getNewTaskNumber() { + $s = $this->getDefaultTaskSequence(); + return $s->next($this->getDefaultTaskNumberFormat(), + array('Task', 'isNumberUnique')); + } + /* autoresponders & Alerts */ function autoRespONNewTicket() { return ($this->get('ticket_autoresponder')); diff --git a/include/class.dept.php b/include/class.dept.php index bd77e48ec3ec973cfbc3d33211836006f00be127..8abf4919468b4c9a9df8ec3f3dc84e37187f9c6c 100644 --- a/include/class.dept.php +++ b/include/class.dept.php @@ -20,6 +20,10 @@ class Dept extends VerySimpleModel { 'table' => DEPT_TABLE, 'pk' => array('id'), 'joins' => array( + 'parent' => array( + 'constraint' => array('pid' => 'Dept.id'), + 'null' => true, + ), 'email' => array( 'constraint' => array('email_id' => 'EmailModel.email_id'), 'null' => true, @@ -90,6 +94,10 @@ class Dept extends VerySimpleModel { return _H(sprintf('dept.%s.%s', $subtag, $this->getId())); } + function getFullName() { + return self::getNameById($this->getId()); + } + function getEmailId() { return $this->email_id; } @@ -255,7 +263,6 @@ class Dept extends VerySimpleModel { if (!isset($this->_groupids)) { $this->_groupids = array(); - $groups = GroupDeptAccess::objects() ->filter(array('dept_id' => $this->getId())) ->values_flat('group_id'); @@ -344,6 +351,34 @@ class Dept extends VerySimpleModel { return $this->getName(); } + function getParent() { + return static::lookup($this->ht['pid']); + } + + /** + * getFullPath + * + * Utility function to retrieve a '/' separated list of department IDs + * in the ancestry of this department. This is used to populate the + * `path` field in the database and is used for access control rather + * than the ID field since nesting of departments is necessary and + * department access can be cascaded. + * + * Returns: + * Slash-separated string of ID ancestry of this department. The string + * always starts and ends with a slash, and will always contain the ID + * of this department last. + */ + function getFullPath() { + $path = ''; + if ($p = $this->getParent()) + $path .= $p->getFullPath(); + else + $path .= '/'; + $path .= $this->getId() . '/'; + return $path; + } + /*----Static functions-------*/ static function getIdByName($name) { $row = static::objects() @@ -355,11 +390,8 @@ class Dept extends VerySimpleModel { } function getNameById($id) { - - if($id && ($dept=static::lookup($id))) - $name= $dept->getName(); - - return $name; + $names = static::getDepartments(); + return $names[$id]; } function getDefaultDeptName() { @@ -372,10 +404,11 @@ class Dept extends VerySimpleModel { : null; } - static function getDepartments( $criteria=null) { + static function getDepartments( $criteria=null, $localize=true) { static $depts = null; if (!isset($depts) || $criteria) { + // XXX: This will upset the static $depts array $depts = array(); $query = self::objects(); if (isset($criteria['publiconly'])) @@ -395,17 +428,39 @@ class Dept extends VerySimpleModel { } $query->order_by('name') - ->values_flat('id', 'name'); + ->values('id', 'pid', 'name', 'parent'); - $names = array(); foreach ($query as $row) - $names[$row[0]] = $row[1]; + $depts[$row['id']] = $row; + + $localize_this = function($id, $default) use ($localize) { + if (!$localize) + return $default; + + $tag = _H("dept.name.{$id}"); + $T = CustomDataTranslation::translate($tag); + return $T != $tag ? $T : $default; + }; - // Fetch local names - foreach (CustomDataTranslation::getDepartmentNames(array_keys($names)) as $id=>$name) { - // Translate the department - $names[$id] = $name; + // Resolve parent names + $names = array(); + foreach ($depts as $id=>$info) { + $name = $info['name']; + $loop = array($id=>true); + $parent = false; + while ($info['pid'] && ($info = $depts[$info['pid']])) { + $name = sprintf('%s / %s', $info['name'], $name); + if (isset($loop[$info['pid']])) + break; + $loop[$info['pid']] = true; + $parent = $info; + } + // Fetch local names + $names[$id] = $localize_this($id, $name); } + asort($names); + + // TODO: Use locale-aware sorting mechanism if ($criteria) return $names; @@ -461,9 +516,13 @@ class Dept extends VerySimpleModel { if (!$vars['ispublic'] && $cfg && ($vars['id']==$cfg->getDefaultDeptId())) $errors['ispublic']=__('System default department cannot be private'); + if ($vars['pid'] && !($p = static::lookup($vars['pid']))) + $errors['pid'] = __('Department selection is required'); + if ($errors) return false; + $this->pid = $vars['pid']; $this->updated = SqlFunction::NOW(); $this->ispublic = isset($vars['ispublic'])?$vars['ispublic']:0; $this->email_id = isset($vars['email_id'])?$vars['email_id']:0; diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php index 2aeacc7ac46bdbc73f151fecb4bd11ce3dc57823..be4f4af48565884d8d8c76775f0a57d8744ce0d4 100644 --- a/include/class.dynamic_forms.php +++ b/include/class.dynamic_forms.php @@ -698,7 +698,6 @@ class DynamicFormField extends VerySimpleModel { static function create($ht=false) { $inst = parent::create($ht); $inst->set('created', new SqlFunction('NOW')); - $inst->flags = self::FLAG_ENABLED; if (isset($ht['configuration'])) $inst->configuration = JsonDataEncoder::encode($ht['configuration']); return $inst; @@ -920,11 +919,6 @@ class DynamicFormEntry extends VerySimpleModel { $this->object_id = $ticket_id; } - function forClient($user_id) { - return DynamicFormEntry::objects() - ->filter(array('object_id'=>$user_id, 'object_type'=>'U')); - } - function setClientId($user_id) { $this->object_type = 'U'; $this->object_id = $user_id; @@ -934,14 +928,9 @@ class DynamicFormEntry extends VerySimpleModel { $this->object_id = $object_id; } - function forUser($user_id) { + function forObject($object_id, $object_type) { return DynamicFormEntry::objects() - ->filter(array('object_id'=>$user_id, 'object_type'=>'U')); - } - - function forOrganization($org_id) { - return DynamicFormEntry::objects() - ->filter(array('object_id'=>$org_id, 'object_type'=>'O')); + ->filter(array('object_id'=>$object_id, 'object_type'=>$object_type)); } function render($staff=true, $title=false, $options=array()) { @@ -1392,9 +1381,10 @@ class SelectionField extends FormField { } class TypeaheadSelectionWidget extends ChoicesWidget { - function render($how) { - if ($how == 'search') - return parent::render($how); + function render($options=array()) { + + if ($options['mode'] == 'search') + return parent::render($options); $name = $this->getEnteredValue(); $config = $this->field->getConfiguration(); diff --git a/include/class.export.php b/include/class.export.php index 6f8ec713f3437a2cb21a264a80aa4637966ddd7e..51aec30250c6921671b3f76fbdaf0c9a3323b55d 100644 --- a/include/class.export.php +++ b/include/class.export.php @@ -316,8 +316,8 @@ class DatabaseExporter { GROUP_DEPT_TABLE, TEAM_TABLE, TEAM_MEMBER_TABLE, FAQ_TABLE, FAQ_TOPIC_TABLE, FAQ_CATEGORY_TABLE, DRAFT_TABLE, CANNED_TABLE, TICKET_TABLE, ATTACHMENT_TABLE, - TICKET_THREAD_TABLE, TICKET_ATTACHMENT_TABLE, TICKET_PRIORITY_TABLE, - TICKET_LOCK_TABLE, TICKET_EVENT_TABLE, TICKET_EMAIL_INFO_TABLE, + THREAD_TABLE, THREAD_ENTRY_TABLE, THREAD_ENTRY_EMAIL_TABLE, + TICKET_LOCK_TABLE, TICKET_EVENT_TABLE, TICKET_PRIORITY_TABLE, EMAIL_TABLE, EMAIL_TEMPLATE_TABLE, EMAIL_TEMPLATE_GRP_TABLE, FILTER_TABLE, FILTER_RULE_TABLE, SLA_TABLE, API_KEY_TABLE, TIMEZONE_TABLE, SESSION_TABLE, PAGE_TABLE, diff --git a/include/class.file.php b/include/class.file.php index 15f4ea17a0f8dc294672bfa4d695a7a12dc1ff09..caafae2bf9cab7018b6aee5d4a8503d2f7d84295 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -29,11 +29,14 @@ class AttachmentFile { if(!$id && !($id=$this->getId())) return false; - $sql='SELECT id, f.type, size, name, `key`, signature, ft, bk, f.created, ' - .' count(DISTINCT a.object_id) as canned, count(DISTINCT t.ticket_id) as tickets ' + $sql='SELECT f.id, f.type, size, name, `key`, signature, ft, bk, f.created, ' + .' count(DISTINCT a.object_id) as canned, ' + .' count(DISTINCT t.id) as entries ' .' FROM '.FILE_TABLE.' f ' - .' LEFT JOIN '.ATTACHMENT_TABLE.' a ON(a.file_id=f.id) ' - .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' t ON(t.file_id=f.id) ' + .' LEFT JOIN '.ATTACHMENT_TABLE.' a + ON(a.file_id=f.id) ' + .' LEFT JOIN '.ATTACHMENT_TABLE.' t + ON(t.file_id = f.id) ' .' WHERE f.id='.db_input($id) .' GROUP BY f.id'; if(!($res=db_query($sql)) || !db_num_rows($res)) @@ -57,8 +60,8 @@ class AttachmentFile { return $this->getHashtable(); } - function getNumTickets() { - return $this->ht['tickets']; + function getNumEntries() { + return $this->ht['entries']; } function isCanned() { @@ -66,7 +69,7 @@ class AttachmentFile { } function isInUse() { - return ($this->getNumTickets() || $this->isCanned()); + return ($this->getNumEntries() || $this->isCanned()); } function getId() { @@ -576,9 +579,8 @@ class AttachmentFile { // XXX: Allow plugins to define filetypes which do not represent // files attached to tickets or other things in the attachment // table and are not logos + //FIXME: Just user straight up left join $sql = 'SELECT id FROM '.FILE_TABLE.' WHERE id NOT IN (' - .'SELECT file_id FROM '.TICKET_ATTACHMENT_TABLE - .' UNION ' .'SELECT file_id FROM '.ATTACHMENT_TABLE .") AND `ft` = 'T' AND TIMESTAMPDIFF(DAY, `created`, CURRENT_TIMESTAMP) > 1"; diff --git a/include/class.forms.php b/include/class.forms.php index 60aca0e94dbbbec9491b9380f8732e2e7ea1f425..4188797bddf25a7496b4e6a382224c1651b8e296 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -95,6 +95,7 @@ class Form { foreach ($this->getFields() as $key=>$field) { if (!$field->hasData()) continue; + $this->_clean[$key] = $this->_clean[$field->get('name')] = $field->getClean(); } @@ -653,15 +654,15 @@ class FormField { return array(); } - function render($mode=null) { - $rv = $this->getWidget()->render($mode); + function render($options=array()) { + $rv = $this->getWidget()->render($options); if ($v = $this->get('visibility')) { $v->emitJavascript($this); } return $rv; } - function renderExtras($mode=null) { + function renderExtras($options=array()) { return; } @@ -1458,7 +1459,7 @@ class ThreadEntryField extends FormField { 'label'=>__('Enable Attachments'), 'default'=>$cfg->allowAttachments(), 'configuration'=>array( - 'desc'=>__('Enables attachments on tickets, regardless of channel'), + 'desc'=>__('Enables attachments, regardless of channel'), ), 'validators' => function($self, $value) { if (!ini_get('file_uploads')) @@ -1546,6 +1547,139 @@ FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() { }); +class DepartmentField extends ChoiceField { + function getWidget() { + $widget = parent::getWidget(); + if ($widget->value instanceof Dept) + $widget->value = $widget->value->getId(); + return $widget; + } + + function hasIdValue() { + return true; + } + + function getChoices() { + global $cfg; + + $choices = array(); + if (($depts = Dept::getDepartments())) + foreach ($depts as $id => $name) + $choices[$id] = $name; + + return $choices; + } + + function parse($id) { + return $this->to_php(null, $id); + } + + function to_php($value, $id=false) { + if (is_array($id)) { + reset($id); + $id = key($id); + } + return $id; + } + + function to_database($dept) { + return ($dept instanceof Dept) + ? array($dept->getName(), $dept->getId()) + : $dept; + } + + function toString($value) { + return (string) $value; + } + + function searchable($value) { + return null; + } + + function getConfigurationOptions() { + return array( + 'prompt' => new TextboxField(array( + 'id'=>2, 'label'=>__('Prompt'), 'required'=>false, 'default'=>'', + 'hint'=>__('Leading text shown before a value is selected'), + 'configuration'=>array('size'=>40, 'length'=>40), + )), + ); + } +} +FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() { + return array( + 'department' => array(__('Department'), DepartmentField), + ); +}); + + +class AssigneeField extends ChoiceField { + function getWidget() { + $widget = parent::getWidget(); + if (is_object($widget->value)) + $widget->value = $widget->value->getId(); + return $widget; + } + + function hasIdValue() { + return true; + } + + function getChoices() { + global $cfg; + $choices = array(); + if (($agents = Staff::getAvailableStaffMembers())) + foreach ($agents as $id => $name) + $choices[$id] = $name; + + return $choices; + } + + function parse($id) { + return $this->to_php(null, $id); + } + + function to_php($value, $id=false) { + if (is_array($id)) { + reset($id); + $id = key($id); + } + + return $id; + } + + + function to_database($value) { + return (is_object($value)) + ? array($value->getName(), $value->getId()) + : $value; + } + + function toString($value) { + return (string) $value; + } + + function searchable($value) { + return null; + } + + function getConfigurationOptions() { + return array( + 'prompt' => new TextboxField(array( + 'id'=>2, 'label'=>__('Prompt'), 'required'=>false, 'default'=>'', + 'hint'=>__('Leading text shown before a value is selected'), + 'configuration'=>array('size'=>40, 'length'=>40), + )), + ); + } +} +FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() { + return array( + 'assignee' => array(__('Assignee'), AssigneeField), + ); +}); + + class TicketStateField extends ChoiceField { static $_states = array( @@ -2130,7 +2264,7 @@ class Widget { class TextboxWidget extends Widget { static $input_type = 'text'; - function render($mode=false) { + function render($options=array()) { $config = $this->field->getConfiguration(); if (isset($config['size'])) $size = "size=\"{$config['size']}\""; @@ -2172,7 +2306,7 @@ class PasswordWidget extends TextboxWidget { } class TextareaWidget extends Widget { - function render($mode=false) { + function render($options=array()) { $config = $this->field->getConfiguration(); $class = $cols = $rows = $maxlength = ""; if (isset($config['rows'])) @@ -2201,7 +2335,7 @@ class TextareaWidget extends Widget { } class PhoneNumberWidget extends Widget { - function render($mode=false) { + function render($options=array()) { $config = $this->field->getConfiguration(); list($phone, $ext) = explode("X", $this->value); ?> @@ -2229,7 +2363,9 @@ class PhoneNumberWidget extends Widget { } class ChoicesWidget extends Widget { - function render($mode=false) { + function render($options=array()) { + + $mode = isset($options['mode']) ? $options['mode'] : null; if ($mode == 'view') { if (!($val = (string) $this->field)) @@ -2333,7 +2469,7 @@ class CheckboxWidget extends Widget { $this->name = '_field-checkboxes'; } - function render($mode=false) { + function render($options=array()) { $config = $this->field->getConfiguration(); if (!isset($this->value)) $this->value = $this->field->get('default'); @@ -2362,7 +2498,7 @@ class CheckboxWidget extends Widget { } class DatetimePickerWidget extends Widget { - function render($mode=false) { + function render($options=array()) { global $cfg; $config = $this->field->getConfiguration(); @@ -2439,7 +2575,7 @@ class DatetimePickerWidget extends Widget { } class SectionBreakWidget extends Widget { - function render($mode=false) { + function render($options=array()) { ?><div class="form-header section-break"><h3><?php echo Format::htmlchars($this->field->getLocal('label')); ?></h3><em><?php echo Format::htmlchars($this->field->getLocal('hint')); @@ -2449,17 +2585,18 @@ class SectionBreakWidget extends Widget { } class ThreadEntryWidget extends Widget { - function render($client=null) { + function render($options=array()) { global $cfg; $object_id = false; - if (!$client) { - $namespace = 'ticket.staff'; - } - else { - $namespace = 'ticket.client'; - $object_id = substr(session_id(), -12); + if ($options['client']) { + $namespace = $options['draft-namespace'] + ?: 'ticket.client'; + $object_id = substr(session_id(), -12); + } else { + $namespace = $options['draft-namespace'] ?: 'ticket.staff'; } + list($draft, $attrs) = Draft::getDraftAndDataAttrs($namespace, $object_id, $this->value); ?> <span class="required"><?php @@ -2477,7 +2614,7 @@ class ThreadEntryWidget extends Widget { return; $attachments = $this->getAttachments($config); - print $attachments->render($client); + print $attachments->render($options); foreach ($attachments->getMedia() as $type=>$urls) { foreach ($urls as $url) Form::emitMedia($url, $type); @@ -2505,7 +2642,7 @@ class FileUploadWidget extends Widget { ), ); - function render($how) { + function render($options) { $config = $this->field->getConfiguration(); $name = $this->field->getFormName(); $id = substr(md5(spl_object_hash($this)), 10); @@ -2607,7 +2744,7 @@ class FreeTextField extends FormField { } class FreeTextWidget extends Widget { - function render($mode=false) { + function render($options=array()) { $config = $this->field->getConfiguration(); ?><div class=""><h3><?php echo Format::htmlchars($this->field->getLocal('label')); diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php index 1210bffa3f3bfd924169e87939d369c229a4a9ce..77725c0729e9100733aacfc3c6d58feebb64d032 100644 --- a/include/class.mailfetch.php +++ b/include/class.mailfetch.php @@ -515,7 +515,7 @@ class MailFetcher { foreach ($struct->parameters as $p) { if (strtolower($p->attribute) == 'report-type' && $p->value == 'delivery-status') { - return new TextThreadBody( $this->getPart( + return new TextThreadEntryBody( $this->getPart( $mid, 'text/plain', $this->charset, $struct, false, 1)); } } @@ -555,19 +555,19 @@ class MailFetcher { if ($cfg->isHtmlThreadEnabled()) { if ($html=$this->getPart($mid, 'text/html', $this->charset)) - $body = new HtmlThreadBody($html); + $body = new HtmlThreadEntryBody($html); elseif ($text=$this->getPart($mid, 'text/plain', $this->charset)) - $body = new TextThreadBody($text); + $body = new TextThreadEntryBody($text); } elseif ($text=$this->getPart($mid, 'text/plain', $this->charset)) - $body = new TextThreadBody($text); + $body = new TextThreadEntryBody($text); elseif ($html=$this->getPart($mid, 'text/html', $this->charset)) - $body = new TextThreadBody( + $body = new TextThreadEntryBody( Format::html2text(Format::safe_html($html), 100, false)); if (!isset($body)) - $body = new TextThreadBody(''); + $body = new TextThreadEntryBody(''); if ($cfg->stripQuotedReply()) $body->stripQuotedReply($cfg->getReplySeparator()); diff --git a/include/class.mailparse.php b/include/class.mailparse.php index 4038f1226fa53b3a038bbea912ada03e3ceaf108..c2b51d7f07b46523bebc22292388d8bc494ecc62 100644 --- a/include/class.mailparse.php +++ b/include/class.mailparse.php @@ -304,19 +304,19 @@ class Mail_Parse { if ($cfg && $cfg->isHtmlThreadEnabled()) { if ($html=$this->getPart($this->struct,'text/html')) - $body = new HtmlThreadBody($html); + $body = new HtmlThreadEntryBody($html); elseif ($text=$this->getPart($this->struct,'text/plain')) - $body = new TextThreadBody($text); + $body = new TextThreadEntryBody($text); } elseif ($text=$this->getPart($this->struct,'text/plain')) - $body = new TextThreadBody($text); + $body = new TextThreadEntryBody($text); elseif ($html=$this->getPart($this->struct,'text/html')) - $body = new TextThreadBody( + $body = new TextThreadEntryBody( Format::html2text(Format::safe_html($html), 100, false)); if (!isset($body)) - $body = new TextThreadBody(''); + $body = new TextThreadEntryBody(''); if ($cfg && $cfg->stripQuotedReply()) $body->stripQuotedReply($cfg->getReplySeparator()); diff --git a/include/class.model.php b/include/class.model.php new file mode 100644 index 0000000000000000000000000000000000000000..5d41a05d099684b943c9ad754f2b8653b443324d --- /dev/null +++ b/include/class.model.php @@ -0,0 +1,63 @@ +<?php +/********************************************************************* + class.model.php + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2014 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +// TODO: Make ObjectModel models base class and extend VerySimpleModel +class ObjectModel { + + const OBJECT_TYPE_TICKET = 'T'; + const OBJECT_TYPE_THREAD = 'H'; + const OBJECT_TYPE_USER = 'U'; + const OBJECT_TYPE_ORG = 'O'; + const OBJECT_TYPE_FAQ = 'K'; + const OBJECT_TYPE_FILE = 'F'; + const OBJECT_TYPE_TASK = 'A'; + + private function objects() { + static $objects = false; + if ($objects == false) { + $objects = array( + self::OBJECT_TYPE_TICKET => 'Ticket', + self::OBJECT_TYPE_THREAD => 'ThreadEntry', + self::OBJECT_TYPE_USER => 'User', + self::OBJECT_TYPE_ORG => 'Organization', + self::OBJECT_TYPE_FAQ => 'FAQ', + self::OBJECT_TYPE_FILE => 'AttachmentFile', + self::OBJECT_TYPE_TASK => 'Task', + ); + } + + return $objects; + } + + static function getType($model) { + + foreach (self::objects() as $t => $c) { + if ($model instanceof $c) + return $t; + } + } + + static function lookup($id, $type) { + $model = null; + if ($id + && ($objects=self::objects()) + && ($class=$objects[$type]) + && class_exists($class) + && is_callable(array($class, 'lookup'))) + $model = $class::lookup($id); + + return $model; + } +} +?> diff --git a/include/class.orm.php b/include/class.orm.php index 37178b276c6513cedd70458ca340f7020f19adc1..6ed7d52f164bd5f2687207ab78a1bb425759a3db 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -1243,17 +1243,8 @@ class InstrumentedList extends ModelInstanceManager { } // QuerySet overriedes - function filter() { - return call_user_func_array(array($this->objects(), 'filter'), func_get_args()); - } - function exclude() { - return call_user_func_array(array($this->objects(), 'exclude'), func_get_args()); - } - function order_by() { - return call_user_func_array(array($this->objects(), 'order_by'), func_get_args()); - } - function limit($how) { - return $this->objects()->limit($how); + function __call($what, $how) { + return call_user_func_array(array($this->objects(), $what), $how); } } diff --git a/include/class.role.php b/include/class.role.php index ef424a917d5cd45a95e0186977cd6e578893005c..38fbc68a93465250611c7165a9c7e842ca5b207e 100644 --- a/include/class.role.php +++ b/include/class.role.php @@ -281,6 +281,26 @@ class RolePermission { /* @trans */ 'Delete', /* @trans */ 'Ability to delete tickets'), ), + /* @trans */ 'Tasks' => array( + 'task.create' => array( + /* @trans */ 'Create', + /* @trans */ 'Ability to create tasks'), + 'task.edit' => array( + /* @trans */ 'Edit', + /* @trans */ 'Ability to edit tasks'), + 'task.assign' => array( + /* @trans */ 'Assign', + /* @trans */ 'Ability to assign tasks to agents or teams'), + 'task.transfer' => array( + /* @trans */ 'Transfer', + /* @trans */ 'Ability to transfer tasks between departments'), + 'task.close' => array( + /* @trans */ 'Close', + /* @trans */ 'Ability to close tasks'), + 'task.delete' => array( + /* @trans */ 'Delete', + /* @trans */ 'Ability to delete tasks'), + ), /* @trans */ 'Knowledgebase' => array( 'kb.premade' => array( /* @trans */ 'Premade', @@ -353,7 +373,7 @@ class RolePermission { return ($this->has('ticket.transfer')); } - function canPostReply() { + function canPostTicketReply() { return ($this->has('ticket.reply')); } @@ -365,6 +385,35 @@ class RolePermission { return ($this->has('ticket.delete')); } + /* tasks */ + function canCreateTasks() { + return ($this->get('task.create')); + } + + function canEditTasks() { + return ($this->get('task.edit')); + } + + function canAssignTasks() { + return ($this->get('task.assign')); + } + + function canTransferTasks() { + return ($this->get('task.transfer')); + } + + function canPostTaskReply() { + return ($this->get('task.reply')); + } + + function canCloseTasks() { + return ($this->get('task.close')); + } + + function canDeleteTasks() { + return ($this->get('task.delete')); + } + /* Knowledge base */ function canManagePremade() { return ($this->has('kb.premade')); diff --git a/include/class.search.php b/include/class.search.php index d4bb2a723c298ab3e267f446b68e277644cc73ba..86ebf80654aaaefe3fb23a6d71121414b5ca5855 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -92,7 +92,8 @@ class SearchInterface { $model->getBody()->getSearchable(), $new, array( 'title' => $model->getTitle(), - 'ticket_id' => $model->getTicketId(), + //TODO: send attribute of object_id + 'ticket_id' => $model->getThread()->getObjectId(), 'created' => $model->getCreateDate(), ) ); @@ -218,31 +219,15 @@ class MysqlSearchBackend extends SearchBackend { } function update($model, $id, $content, $new=false, $attrs=array()) { - switch (true) { - case $model instanceof ThreadEntry: - $type = 'H'; - break; - case $model instanceof Ticket: + + + if (!($type=ObjectModel::getType($model))) + return; + + if ($model instanceof Ticket) $attrs['title'] = $attrs['number'].' '.$attrs['title']; - $type = 'T'; - break; - case $model instanceof User: + elseif ($model instanceof User) $content .= implode("\n", $attrs['emails']); - $type = 'U'; - break; - case $model instanceof Organization: - $type = 'O'; - break; - case $model instanceof FAQ: - $type = 'K'; - break; - case $model instanceof AttachmentFile: - $type = 'F'; - break; - default: - // Not indexed - return; - } $title = $attrs['title'] ?: ''; @@ -374,8 +359,7 @@ class MysqlSearchBackend extends SearchBackend { }; // THREADS ---------------------------------- - - $sql = "SELECT A1.`id`, A1.`title`, A1.`body`, A1.`format` FROM `".TICKET_THREAD_TABLE."` A1 + $sql = "SELECT A1.`id`, A1.`title`, A1.`body`, A1.`format` FROM `".THREAD_ENTRY_TABLE."` A1 LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`id` = A2.`object_id` AND A2.`object_type`='H') WHERE A2.`object_id` IS NULL AND (A1.poster <> 'SYSTEM') AND (LENGTH(A1.`title`) + LENGTH(A1.`body`) > 0) @@ -384,7 +368,7 @@ class MysqlSearchBackend extends SearchBackend { return false; while ($row = db_fetch_row($res)) { - $body = ThreadBody::fromFormattedText($row[2], $row[3]); + $body = ThreadEntryBody::fromFormattedText($row[2], $row[3]); $body = $body->getSearchable(); $title = Format::searchable($row[1]); if (!$body && !$title) diff --git a/include/class.staff.php b/include/class.staff.php index 4fa79eb6a9a62e83065c1c7b80d92cd9b6b1832a..9a8ac128b05ca7f6842c0ea361c2bb3f071608c8 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -208,28 +208,50 @@ implements AuthenticatedUser { } function getDepartments() { + // TODO: Cache this in the agent's session as it is unlikely to + // change while logged in if (!isset($this->departments)) { // Departments the staff is "allowed" to access... // based on the group they belong to + user's primary dept + user's managed depts. - $dept_ids = array(); - $depts = Dept::objects() - ->filter(Q::any(array( - 'id' => $this->dept_id, - 'groups__group_id' => $this->group_id, - 'manager_id' => $this->getId(), - ))) + $sql='SELECT DISTINCT d.id FROM '.STAFF_TABLE.' s ' + .' LEFT JOIN '.GROUP_DEPT_TABLE.' g ON (s.group_id=g.group_id) ' + .' INNER JOIN '.DEPT_TABLE.' d ON (LOCATE(CONCAT("/", s.dept_id, "/"), d.path) OR d.manager_id=s.staff_id OR LOCATE(CONCAT("/", g.dept_id, "/"), d.path)) ' + .' WHERE s.staff_id='.db_input($this->getId()); + $depts = array(); + if (($res=db_query($sql)) && db_num_rows($res)) { + while(list($id)=db_fetch_row($res)) + $depts[] = $id; + } + + /* ORM method — about 2.0ms slower + $q = Q::any(array( + 'path__contains' => '/'.$this->dept_id.'/', + 'manager_id' => $this->getId(), + )); + // Add in group access + foreach ($this->group->depts->values_flat('dept_id') as $row) { + // Skip primary dept + if ($row[0] == $this->dept_id) + continue; + $q->add(new Q(array('path__contains'=>'/'.$row[0].'/'))); + } + + $dept_ids = Dept::objects() + ->filter($q) + ->distinct('id') ->values_flat('id'); - foreach ($depts as $row) - $dept_ids[] = $row[0]; + foreach ($dept_ids as $row) + $depts[] = $row[0]; + */ - if (!$dept_ids) { //Neptune help us! (fallback) - $dept_ids = array_merge($this->getGroup()->getDepartments(), array($this->getDeptId())); + if (!$depts) { //Neptune help us! (fallback) + $depts = array_merge($this->getGroup()->getDepartments(), array($this->getDeptId())); } - $this->departments = array_filter(array_unique($dept_ids)); + $this->departments = $depts; } return $this->departments; @@ -564,6 +586,11 @@ implements AuthenticatedUser { if (!parent::delete()) return false; + //Update the poster and clear staff_id on ticket thread table. + db_query('UPDATE '.THREAD_ENTRY_TABLE + .' SET staff_id=0, poster= '.db_input($this->getName()->getOriginal()) + .' WHERE staff_id='.db_input($this->getId())); + // DO SOME HOUSE CLEANING //Move remove any ticket assignments...TODO: send alert to Dept. manager? db_query('UPDATE '.TICKET_TABLE.' SET staff_id=0 WHERE staff_id='.db_input($this->getId())); diff --git a/include/class.task.php b/include/class.task.php new file mode 100644 index 0000000000000000000000000000000000000000..9b155e249a94b9b20ebf89295b76b23e26d05485 --- /dev/null +++ b/include/class.task.php @@ -0,0 +1,494 @@ +<?php + +class TaskModel extends VerySimpleModel { + static $meta = array( + 'table' => TASK_TABLE, + 'pk' => array('id'), + 'joins' => array( + 'dept' => array( + 'constraint' => array('dept_id' => 'Dept.id'), + ), + 'staff' => array( + 'constraint' => array('staff_id' => 'Staff.staff_id'), + 'null' => true, + ), + ), + ); + + const ISOPEN = 0x0001; + const ISOVERDUE = 0x0002; + + + protected function hasFlag($flag) { + return ($this->get('flags') & $flag) !== 0; + } + + protected function clearFlag($flag) { + return $this->set('flags', $this->get('flags') & ~$flag); + } + + protected function setFlag($flag) { + return $this->set('flags', $this->get('flags') | $flag); + } + + function getId() { + return $this->id; + } + + function getNumber() { + return $this->number; + } + + function getStaffId() { + return $this->staff_id; + } + + function getTeamId() { + return $this->team_id; + } + + function getDeptId() { + return $this->dept_id; + } + + function getCreateDate() { + return $this->created; + } + + function getDueDate() { + return $this->duedate; + } + + function isOpen() { + return $this->hasFlag(self::ISOPEN); + } + + function isClosed() { + return !$this->isOpen(); + } + + function close() { + return $this->clearFlag(self::ISOPEN); + } + + function reopen() { + return $this->setFlag(self::ISOPEN); + } + + function isAssigned() { + return ($this->isOpen() && ($this->getStaffId() || $this->getTeamId())); + } + + function isOverdue() { + return $this->hasFlag(self::ISOVERDUE); + } + +} + +class Task extends TaskModel { + var $form; + var $entry; + var $thread; + + var $_entries; + + + function getStatus() { + return $this->isOpen() ? _('Open') : _('Closed'); + } + + function getTitle() { + return $this->__cdata('title', ObjectModel::OBJECT_TYPE_TASK); + } + + function getAssignees() { + + $assignees=array(); + if ($this->staff) + $assignees[] = $this->staff->getName(); + + //Add team assignment + if (isset($this->team)) + $assignees[] = $this->team->getName(); + + return $assignees; + } + + function getAssigned($glue='/') { + $assignees = $this->getAssignees(); + + return $assignees ? implode($glue, $assignees):''; + } + + function getThread() { + + if (!$this->thread) + $this->thread = TaskThread::lookup(array( + 'object_id' => $this->getId(), + 'object_type' => ObjectModel::OBJECT_TYPE_TASK) + ); + + return $this->thread; + } + + function getThreadEntry($id) { + return $this->getThread()->getEntry($id); + } + + function getThreadEntries($type, $order='') { + return $this->getThread()->getEntries( + array('type' => $type, 'order' => $order)); + } + + function getForm() { + if (!isset($this->form)) { + // Look for the entry first + if ($this->form = DynamicFormEntry::lookup( + array('object_type' => ObjectModel::OBJECT_TYPE_TASK))) { + return $this->form; + } + // Make sure the form is in the database + elseif (!($this->form = DynamicForm::lookup( + array('type' => ObjectModel::OBJECT_TYPE_TASK)))) { + $this->__loadDefaultForm(); + return $this->getForm(); + } + // Create an entry to be saved later + $this->form = $this->form->instanciate(); + $this->form->object_type = ObjectModel::OBJECT_TYPE_TASK; + } + + return $this->form; + } + + function addDynamicData($data) { + + $tf = TaskForm::getInstance($this->id, true); + foreach ($tf->getFields() as $f) + if (isset($data[$f->get('name')])) + $tf->setAnswer($f->get('name'), $data[$f->get('name')]); + + $tf->save(); + + return $tf; + } + + function getDynamicData($create=true) { + if (!isset($this->_entries)) { + $this->_entries = DynamicFormEntry::forObject($this->id, + ObjectModel::OBJECT_TYPE_TASK)->all(); + if (!$this->_entries && $create) { + $f = TaskForm::getInstance($this->id, true); + $f->save(); + $this->_entries[] = $f; + } + } + + return $this->_entries ?: array(); + } + + + function to_json() { + + $info = array( + 'id' => $this->getId(), + 'title' => $this->getTitle() + ); + + return JsonDataEncoder::encode($info); + } + + function __cdata($field, $ftype=null) { + + foreach ($this->getDynamicData() as $e) { + // Make sure the form type matches + if (!$e->getForm() + || ($ftype && $ftype != $e->getForm()->get('type'))) + continue; + + // Get the named field and return the answer + if ($f = $e->getForm()->getField($field)) + return $f->getAnswer(); + } + + return null; + } + + function __toString() { + return (string) $this->getTitle(); + } + + /* util routines */ + function assign($vars, &$errors) { + global $thisstaff; + + if (!isset($vars['staff_id']) || !($staff=Staff::lookup($vars['staff_id']))) + $errors['staff_id'] = __('Agent selection required'); + elseif ($staff->getid() == $this->getStaffId()) + $errors['dept_id'] = __('Task already assigned to agent'); + else + $this->staff_id = $staff->getId(); + + if ($errors || !$this->save()) + return false; + + // Transfer completed... post internal note. + $title = sprintf(__('Task assigned to %s'), + $staff->getName()); + if ($vars['comments']) { + $note = $vars['comments']; + } else { + $note = $title; + $title = ''; + } + + $this->postNote( + array('note' => $note, 'title' => $title), + $errors, + $thisstaff); + + return true; + } + + function transfer($vars, &$errors) { + global $thisstaff; + + if (!isset($vars['dept_id']) || !($dept=Dept::lookup($vars['dept_id']))) + $errors['dept_id'] = __('Department selection required'); + elseif ($dept->getid() == $this->getDeptId()) + $errors['dept_id'] = __('Task already in the department'); + else + $this->dept_id = $dept->getId(); + + if ($errors || !$this->save()) + return false; + + // Transfer completed... post internal note. + $title = sprintf(__('Task transfered to %s department'), + $dept->getName()); + if ($vars['comments']) { + $note = $vars['comments']; + } else { + $note = $title; + $title = ''; + } + + $this->postNote( + array('note' => $note, 'title' => $title), + $errors, + $thisstaff); + + return true; + } + + function postNote($vars, &$errors, $poster='', $alert=true) { + global $cfg, $thisstaff; + + $vars['staffId'] = 0; + $vars['poster'] = 'SYSTEM'; + if ($poster && is_object($poster)) { + $vars['staffId'] = $poster->getId(); + $vars['poster'] = $poster->getName(); + } elseif ($poster) { //string + $vars['poster'] = $poster; + } + + if (!($note=$this->getThread()->addNote($vars, $errors))) + return null; + + if (isset($vars['task_status'])) { + if ($vars['task_status']) + $this->reopen(); + else + $this->close(); + + $this->save(true); + } + + return $note; + } + + static function lookupIdByNumber($number) { + $sql = 'SELECT id FROM '.TASK_TABLE + .' WHERE `number`='.db_input($number); + list($id) = db_fetch_row(db_query($sql)); + + return $id; + } + + static function isNumberUnique($number) { + return !self::lookupIdByNumber($number); + } + + static function create($vars) { + global $cfg; + + $task = parent::create(array( + 'flags' => self::ISOPEN, + 'object_id' => $vars['object_id'], + 'object_type' => $vars['object_type'], + 'number' => $cfg->getNewTaskNumber(), + 'created' => new SqlFunction('NOW'), + 'updated' => new SqlFunction('NOW'), + )); + // Save internal fields. + if ($vars['internal_formdata']['staff_id']) + $task->staff_id = $vars['internal_formdata']['staff_id']; + if ($vars['internal_formdata']['dept_id']) + $task->dept_id = $vars['internal_formdata']['dept_id']; + if ($vars['internal_formdata']['duedate']) + $task->duedate = $vars['internal_formdata']['duedate']; + + $task->save(true); + + // Add dynamic data + $task->addDynamicData($vars['default_formdata']); + + // Create a thread + message. + $thread = TaskThread::create($task); + $thread->addDescription($vars); + Signal::send('model.created', $task); + + return $task; + } + + function delete($comments='') { + global $ost, $thisstaff; + + $thread = $this->getThread(); + + if (!parent::delete()) + return false; + + $thread->delete(); + + Draft::deleteForNamespace('task.%.' . $this->getId()); + + foreach (DynamicFormEntry::forObject($this->getId(), ObjectModel::OBJECT_TYPE_TASK) as $form) + $form->delete(); + + // Log delete + $log = sprintf(__('Task #%1$s deleted by %2$s'), + $this->getNumber(), + $thisstaff ? $thisstaff->getName() : __('SYSTEM')); + + if ($comments) + $log .= sprintf('<hr>%s', $comments); + + $ost->logDebug( + sprintf( __('Task #%s deleted'), $this->getNumber()), + $log); + + return true; + + } + + static function __loadDefaultForm() { + + require_once INCLUDE_DIR.'class.i18n.php'; + + $i18n = new Internationalization(); + $tpl = $i18n->getTemplate('form.yaml'); + foreach ($tpl->getData() as $f) { + if ($f['type'] == ObjectModel::OBJECT_TYPE_TASK) { + $form = DynamicForm::create($f); + $form->save(); + break; + } + } + } +} + +class TaskForm extends DynamicForm { + static $instance; + static $defaultForm; + static $internalForm; + + static $forms; + + static function objects() { + $os = parent::objects(); + return $os->filter(array('type'=>ObjectModel::OBJECT_TYPE_TASK)); + } + + static function getDefaultForm() { + if (!isset(static::$defaultForm)) { + if (($o = static::objects()) && $o[0]) + static::$defaultForm = $o[0]; + } + + return static::$defaultForm; + } + + static function getInstance($object_id=0, $new=false) { + if ($new || !isset(static::$instance)) + static::$instance = static::getDefaultForm()->instanciate(); + + static::$instance->object_type = ObjectModel::OBJECT_TYPE_TASK; + + if ($object_id) + static::$instance->object_id = $object_id; + + return static::$instance; + } + + static function getInternalForm($source=null) { + if (!isset(static::$internalForm)) + static::$internalForm = new Form(self::getInternalFields(), $source); + + return static::$internalForm; + } + + static function getInternalFields() { + return array( + 'dept_id' => new DepartmentField(array( + 'id'=>1, + 'label' => __('Department'), + 'flags' => hexdec(0X450F3), + 'required' => true, + )), + 'staff_id' => new AssigneeField(array( + 'id'=>2, + 'label' => __('Assignee'), + 'flags' => hexdec(0X450F3), + 'required' => false, + )), + 'duedate' => new DatetimeField(array( + 'id' => 3, + 'label' => __('Due Date'), + 'flags' => hexdec(0X450B3), + 'required' => false, + 'configuration' => array( + 'min' => Misc::gmtime(), + 'time' => true, + 'gmt' => true, + 'future' => true, + ), + )), + + ); + } +} + +// Task thread class +class TaskThread extends ObjectThread { + + function addDescription($vars, &$errors=array()) { + + $vars['threadId'] = $this->getId(); + $vars['message'] = $vars['description']; + unset($vars['description']); + + return MessageThreadEntry::create($vars, $errors); + } + + static function create($task) { + $id = is_object($task) ? $task->getId() : $task; + return parent::create(array( + 'object_id' => $id, + 'object_type' => ObjectModel::OBJECT_TYPE_TASK + )); + } + +} +?> diff --git a/include/class.thread.php b/include/class.thread.php index 7a95ebd3da025a6cd0e08e66aaa35a0fb2a4a748..3ee72e753fdb97bd68a85c3d0fcff7e91c195be1 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -2,7 +2,7 @@ /********************************************************************* class.thread.php - Ticket thread + Thread of things! XXX: Please DO NOT add any ticket related logic! use ticket class. Peter Rotich <peter@osticket.com> @@ -20,127 +20,112 @@ include_once(INCLUDE_DIR.'class.draft.php'); //Ticket thread. class Thread { - var $id; // same as ticket ID. - var $ticket; - - function Thread($ticket) { - - $this->ticket = $ticket; - - $this->id = 0; + var $ht; - $this->load(); + function Thread($criteria) { + $this->load($criteria); } - function load() { + function load($criteria=null) { - if(!$this->getTicketId()) + if (!$criteria && !($criteria=$this->getId())) return null; - $sql='SELECT ticket.ticket_id as id ' - .' ,count(DISTINCT attach.attach_id) as attachments ' - .' ,count(DISTINCT message.id) as messages ' - .' ,count(DISTINCT response.id) as responses ' - .' ,count(DISTINCT note.id) as notes ' - .' FROM '.TICKET_TABLE.' ticket ' - .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (' - .'ticket.ticket_id=attach.ticket_id) ' - .' LEFT JOIN '.TICKET_THREAD_TABLE.' message ON (' - ."ticket.ticket_id=message.ticket_id AND message.thread_type = 'M') " - .' LEFT JOIN '.TICKET_THREAD_TABLE.' response ON (' - ."ticket.ticket_id=response.ticket_id AND response.thread_type = 'R') " - .' LEFT JOIN '.TICKET_THREAD_TABLE.' note ON ( ' - ."ticket.ticket_id=note.ticket_id AND note.thread_type = 'N') " - .' WHERE ticket.ticket_id='.db_input($this->getTicketId()) - .' GROUP BY ticket.ticket_id'; - - if(!($res=db_query($sql)) || !db_num_rows($res)) - return false; + $sql='SELECT thread.* ' + .' ,count(DISTINCT a.id) as attachments ' + .' ,count(DISTINCT entry.id) as entries ' + .' FROM '.THREAD_TABLE.' thread ' + .' LEFT JOIN '.THREAD_ENTRY_TABLE.' entry + ON (entry.thread_id = thread.id) ' + .' LEFT JOIN '.ATTACHMENT_TABLE.' a + ON (a.object_id=entry.id AND a.`type` = "H") '; - $this->ht = db_fetch_array($res); + if (is_numeric($criteria)) + $sql.= ' WHERE thread.id='.db_input($criteria); + else + $sql.= sprintf(' WHERE thread.object_id=%d AND + thread.object_type=%s', + $criteria['object_id'], + db_input($criteria['object_type'])); - $this->id = $this->ht['id']; + $sql.= ' GROUP BY thread.id'; - return true; - } - - function getId() { - return $this->id; - } + $this->ht = array(); + if (($res=db_query($sql)) && db_num_rows($res)) + $this->ht = db_fetch_array($res); - function getTicketId() { - return $this->getTicket()?$this->getTicket()->getId():0; + return ($this->ht); } - function getTicket() { - return $this->ticket; + function reload() { + return $this->load(); } - function getNumAttachments() { - return $this->ht['attachments']; + function getId() { + return $this->ht['id']; } - function getNumMessages() { - return $this->ht['messages']; + function getObjectId() { + return $this->ht['object_id']; } - function getNumResponses() { - return $this->ht['responses']; + function getObjectType() { + return $this->ht['object_type']; } - function getNumNotes() { - return $this->ht['notes']; - } + function getObject() { - function getCount() { - return $this->getNumMessages() + $this->getNumResponses(); - } + if (!$this->_object) + $this->_object = ObjectModel::lookup( + $this->getObjectId(), $this->getObjectType()); - function getMessages() { - return $this->getEntries('M'); + return $this->_object; } - function getResponses() { - return $this->getEntries('R'); + function getNumAttachments() { + return $this->ht['attachments']; } - function getNotes() { - return $this->getEntries('N'); + function getNumEntries() { + return $this->ht['entries']; } - function getEntries($type, $order='ASC') { + function getEntries($criteria) { - if(!$order || !in_array($order, array('DESC','ASC'))) - $order='ASC'; + if (!$criteria['order'] || !in_array($criteria['order'], array('DESC','ASC'))) + $criteria['order'] = 'ASC'; - $sql='SELECT thread.* + $sql='SELECT entry.* , COALESCE(user.name, IF(staff.staff_id, CONCAT_WS(" ", staff.firstname, staff.lastname), NULL)) as name ' - .' ,count(DISTINCT attach.attach_id) as attachments ' - .' FROM '.TICKET_THREAD_TABLE.' thread ' + .' ,count(DISTINCT attach.id) as attachments ' + .' FROM '.THREAD_ENTRY_TABLE.' entry ' .' LEFT JOIN '.USER_TABLE.' user - ON (thread.user_id=user.id) ' + ON (entry.user_id=user.id) ' .' LEFT JOIN '.STAFF_TABLE.' staff - ON (thread.staff_id=staff.staff_id) ' - .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach - ON (thread.ticket_id=attach.ticket_id - AND thread.id=attach.ref_id) ' - .' WHERE thread.ticket_id='.db_input($this->getTicketId()); + ON (entry.staff_id=staff.staff_id) ' + .' LEFT JOIN '.ATTACHMENT_TABLE.' attach + ON (attach.object_id = entry.id AND attach.`type`="H") ' + .' WHERE entry.thread_id='.db_input($this->getId()); - if($type && is_array($type)) - $sql.=' AND thread.thread_type IN('.implode(',', db_input($type)).')'; - elseif($type) - $sql.=' AND thread.thread_type='.db_input($type); + if ($criteria['type'] && is_array($criteria['type'])) + $sql.=' AND entry.`type` IN (' + .implode(',', db_input($criteria['type'])).')'; + elseif ($criteria['type']) + $sql.=' AND entry.`type` = '.db_input($criteria['type']); - $sql.=' GROUP BY thread.id ' - .' ORDER BY thread.created '.$order; + $sql.=' GROUP BY entry.id ' + .' ORDER BY entry.created '.$criteria['order']; + + if ($criteria['limit']) + $sql.=' LIMIT '.$criteria['limit']; $entries = array(); if(($res=db_query($sql)) && db_num_rows($res)) { while($rec=db_fetch_array($res)) { - $rec['body'] = ThreadBody::fromFormattedText($rec['body'], $rec['format']); + $rec['body'] = ThreadEntryBody::fromFormattedText($rec['body'], $rec['format']); $entries[] = $rec; } } @@ -149,82 +134,73 @@ class Thread { } function getEntry($id) { - return ThreadEntry::lookup($id, $this->getTicketId()); - } - - function addNote($vars, &$errors) { - - //Add ticket Id. - $vars['ticketId'] = $this->getTicketId(); - - return Note::create($vars, $errors); - } - - function addMessage($vars, &$errors) { - - $vars['ticketId'] = $this->getTicketId(); - $vars['staffId'] = 0; - - return Message::create($vars, $errors); + return ThreadEntry::lookup($id, $this->getId()); } - function addResponse($vars, &$errors) { - - $vars['ticketId'] = $this->getTicketId(); - $vars['userId'] = 0; - - return Response::create($vars, $errors); - } function deleteAttachments() { - $deleted=0; // Clear reference table - $res=db_query('DELETE FROM '.TICKET_ATTACHMENT_TABLE.' WHERE ticket_id='.db_input($this->getTicketId())); - if ($res && db_affected_rows()) - $deleted = AttachmentFile::deleteOrphans(); + $sql = 'DELETE FROM '.ATTACHMENT_TABLE. ' a ' + . 'INNER JOIN '.THREAD_ENTRY_TABLE.' e + ON(e.id = a.object_id AND a.`type`= "H") ' + . ' WHERE e.thread_id='.db_input($this->getId()); + + $deleted=0; + if (($res=db_query($sql)) && ($deleted=db_affected_rows())) + AttachmentFile::deleteOrphans(); return $deleted; } function delete() { - $sql = 'UPDATE '.TICKET_EMAIL_INFO_TABLE.' mid - INNER JOIN '.TICKET_THREAD_TABLE.' thread ON (thread.id = mid.thread_id) - SET mid.headers = null WHERE thread.ticket_id = ' - .db_input($this->getTicketId()); - db_query($sql); + //Self delete + $sql = 'DELETE FROM '.THREAD_TABLE.' WHERE + id='.db_input($this->getId()); - $res=db_query('DELETE FROM '.TICKET_THREAD_TABLE.' WHERE ticket_id='.db_input($this->getTicketId())); - if(!$res || !db_affected_rows()) + if (!db_query($sql) || !db_affected_rows()) return false; + // Clear email meta data (header..etc) + $sql = 'UPDATE '.THREAD_ENTRY_EMAIL_TABLE.' email ' + . 'INNER JOIN '.THREAD_ENTRY_TABLE.' entry + ON (entry.id = email.thread_entry_id) ' + . 'SET email.headers = null ' + . 'WHERE entry.thread_id = '.db_input($this->getId()); + db_query($sql); + + // Mass delete entries $this->deleteAttachments(); + $sql = 'DELETE FROM '.THREAD_ENTRY_TABLE + . ' WHERE thread_id='.db_input($this->getId()); + db_query($sql); return true; } - /* static */ - function lookup($ticket) { + static function create($vars) { - return ($ticket - && is_object($ticket) - && ($thread = new Thread($ticket)) - && $thread->getId() - )?$thread:null; + if (!$vars || !$vars['object_id'] || !$vars['object_type']) + return false; + + $sql = 'INSERT INTO '.THREAD_TABLE.' SET created=NOW() ' + .', object_id='.db_input($vars['object_id']) + .', object_type='.db_input($vars['object_type']); + + if (db_query($sql)) + return static::lookup(db_insert_id()); + + return null; } - function getVar($name) { - switch ($name) { - case 'original': - return Message::firstByTicketId($this->ticket->getId()) - ->getBody(); - break; - case 'last_message': - case 'lastmessage': - return $this->ticket->getLastMessage()->getBody(); - break; - } + static function lookup($id) { + + return ($id + && ($thread = new Thread($id)) + && $thread->getId() + ) + ? $thread : null; } } @@ -234,47 +210,41 @@ Class ThreadEntry { var $id; var $ht; - var $staff; - var $ticket; - + var $thread; var $attachments; - - function ThreadEntry($id, $type='', $ticketId=0) { - $this->load($id, $type, $ticketId); + function ThreadEntry($id, $threadId=0, $type='') { + $this->load($id, $threadId, $type); } - function load($id=0, $type='', $ticketId=0) { + function load($id=0, $threadId=0, $type='') { - if(!$id && !($id=$this->getId())) + if (!$id && !($id=$this->getId())) return false; - $sql='SELECT thread.*, info.email_mid, info.headers ' - .' ,count(DISTINCT attach.attach_id) as attachments ' - .' FROM '.TICKET_THREAD_TABLE.' thread ' - .' LEFT JOIN '.TICKET_EMAIL_INFO_TABLE.' info - ON (thread.id=info.thread_id) ' - .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach - ON (thread.ticket_id=attach.ticket_id - AND thread.id=attach.ref_id) ' - .' WHERE thread.id='.db_input($id); + $sql='SELECT entry.*, email.mid, email.headers ' + .' ,count(DISTINCT attach.id) as attachments ' + .' FROM '.THREAD_ENTRY_TABLE.' entry ' + .' LEFT JOIN '.THREAD_ENTRY_EMAIL_TABLE.' email + ON (email.thread_entry_id=entry.id) ' + .' LEFT JOIN '.ATTACHMENT_TABLE.' attach + ON (attach.object_id=entry.id AND attach.`type` = "H") ' + .' WHERE entry.id='.db_input($id); - if($type) - $sql.=' AND thread.thread_type='.db_input($type); + if ($type) + $sql.=' AND entry.type='.db_input($type); - if($ticketId) - $sql.=' AND thread.ticket_id='.db_input($ticketId); + if ($threadId) + $sql.=' AND entry.thread_id='.db_input($threadId); - $sql.=' GROUP BY thread.id '; + $sql.=' GROUP BY entry.id '; - if(!($res=db_query($sql)) || !db_num_rows($res)) + if (!($res=db_query($sql)) || !db_num_rows($res)) return false; $this->ht = db_fetch_array($res); $this->id = $this->ht['id']; - - $this->staff = $this->ticket = null; - $this->attachments = array(); + $this->attachments = new GenericAttachments($this->id, 'H'); return true; } @@ -292,7 +262,7 @@ Class ThreadEntry { } function getType() { - return $this->ht['thread_type']; + return $this->ht['type']; } function getSource() { @@ -308,20 +278,20 @@ Class ThreadEntry { } function getBody() { - return ThreadBody::fromFormattedText($this->ht['body'], $this->ht['format']); + return ThreadEntryBody::fromFormattedText($this->ht['body'], $this->ht['format']); } function setBody($body) { global $cfg; - if (!$body instanceof ThreadBody) { + if (!$body instanceof ThreadEntryBody) { if ($cfg->isHtmlThreadEnabled()) - $body = new HtmlThreadBody($body); + $body = new HtmlThreadEntryBody($body); else - $body = new TextThreadBody($body); + $body = new TextThreadEntryBody($body); } - $sql='UPDATE '.TICKET_THREAD_TABLE.' SET updated=NOW()' + $sql='UPDATE '.THREAD_ENTRY_TABLE.' SET updated=NOW()' .',format='.db_input($body->getType()) .',body='.db_input((string) $body) .' WHERE id='.db_input($this->getId()); @@ -340,12 +310,8 @@ Class ThreadEntry { return $this->ht['attachments']; } - function getTicketId() { - return $this->ht['ticket_id']; - } - function getEmailMessageId() { - return $this->ht['email_mid']; + return $this->ht['mid']; } function getEmailHeaderArray() { @@ -398,12 +364,16 @@ Class ThreadEntry { } - function getTicket() { + function getThreadId() { + return $this->ht['thread_id']; + } + + function getThread() { - if(!$this->ticket && $this->getTicketId()) - $this->ticket = Ticket::lookup($this->getTicketId()); + if(!$this->thread && $this->getThreadId()) + $this->thread = Thread::lookup($this->getThreadId()); - return $this->ticket; + return $this->thread; } function getStaffId() { @@ -424,17 +394,8 @@ Class ThreadEntry { function getUser() { - if (!isset($this->user)) { - if (!($ticket = $this->getTicket())) - return null; - - if ($ticket->getOwnerId() == $this->getUserId()) - $this->user = new TicketOwner( - User::lookup($this->getUserId()), $ticket); - else - $this->user = Collaborator::lookup(array( - 'userId'=>$this->getUserId(), 'ticketId'=>$this->getTicketId())); - } + if (!isset($this->user)) + $this->user = User::lookup($this->getUserId()); return $this->user; } @@ -492,7 +453,7 @@ Class ThreadEntry { XXX: We're doing it here because it will eventually become a thread post comment (hint: comments coming!) XXX: logNote must watch for possible loops */ - $this->getTicket()->logNote(__('File Upload Error'), $error, 'SYSTEM', false); + $this->getThread()->getObject()->logNote(__('File Upload Error'), $error, 'SYSTEM', false); } } @@ -522,12 +483,12 @@ Class ThreadEntry { $id=0; if ($attachment['error'] || !($id=$this->saveAttachment($attachment))) { $error = $attachment['error']; - if(!$error) - $error = sprintf(_S('Unable to import attachment - %s'),$attachment['name']); - - $this->getTicket()->logNote(_S('File Import Error'), $error, - _S('SYSTEM'), false); + $error = sprintf(_S('Unable to import attachment - %s'), + $attachment['name']); + //FIXME: logComment here + $this->getThread()->getObject()->logNote( + _S('File Import Error'), $error, _S('SYSTEM'), false); } return $id; @@ -539,87 +500,54 @@ Class ThreadEntry { */ function saveAttachment(&$file) { - if (is_numeric($file)) - $fileId = $file; - elseif (is_array($file) && isset($file['id'])) - $fileId = $file['id']; - elseif (!($fileId = AttachmentFile::save($file))) - return 0; - $inline = is_array($file) && @$file['inline']; - // TODO: Add a unique index to TICKET_ATTACHMENT_TABLE (file_id, - // ref_id), and remove this block - if ($id = db_result(db_query('SELECT attach_id FROM '.TICKET_ATTACHMENT_TABLE - .' WHERE file_id='.db_input($fileId).' AND ref_id=' - .db_input($this->getId())))) - return $id; - - $sql ='INSERT IGNORE INTO '.TICKET_ATTACHMENT_TABLE.' SET created=NOW() ' - .' ,file_id='.db_input($fileId) - .' ,ticket_id='.db_input($this->getTicketId()) - .' ,inline='.db_input($inline ? 1 : 0) - .' ,ref_id='.db_input($this->getId()); - - return (db_query($sql) && ($id=db_insert_id()))?$id:0; + return $this->attachments->save($file, $inline); } function saveAttachments($files) { $ids=array(); - foreach($files as $file) - if(($id=$this->saveAttachment($file))) + foreach ($files as $file) + if (($id=$this->saveAttachment($file))) $ids[] = $id; return $ids; } function getAttachments() { - - if($this->attachments) - return $this->attachments; - - //XXX: inner join the file table instead? - $sql='SELECT a.attach_id, f.id as file_id, f.size, lower(f.`key`) as file_hash, f.name, a.inline ' - .' FROM '.FILE_TABLE.' f ' - .' INNER JOIN '.TICKET_ATTACHMENT_TABLE.' a ON(f.id=a.file_id) ' - .' WHERE a.ticket_id='.db_input($this->getTicketId()) - .' AND a.ref_id='.db_input($this->getId()); - - $this->attachments = array(); - if(($res=db_query($sql)) && db_num_rows($res)) { - while($rec=db_fetch_array($res)) - $this->attachments[] = $rec; - } - - return $this->attachments; + return $this->attachments->getAll(false); } function getAttachmentUrls($script='image.php') { $json = array(); foreach ($this->getAttachments() as $att) { - $json[$att['file_hash']] = array( - 'download_url' => sprintf('attachment.php?id=%d&h=%s', $att['attach_id'], - strtolower(md5($att['file_id'].session_id().$att['file_hash']))), + $json[$att['key']] = array( + 'download_url' => sprintf('attachment.php?id=%d&h=%s', + $att['attach_id'], $att['download']), 'filename' => $att['name'], ); } + return $json; } - function getAttachmentsLinks($file='attachment.php', $target='', $separator=' ') { + function getAttachmentsLinks($file='attachment.php', $target='_blank', $separator=' ') { $str=''; - foreach($this->getAttachments() as $attachment ) { - if ($attachment['inline']) - continue; - /* The hash can be changed but must match validation in @file */ - $hash=md5($attachment['file_id'].session_id().$attachment['file_hash']); + foreach ($this->getAttachments() as $att ) { + if ($att['inline']) continue; $size = ''; - if($attachment['size']) - $size=sprintf('<em>(%s)</em>', Format::file_size($attachment['size'])); + if ($att['size']) + $size=sprintf('<em>(%s)</em>', Format::file_size($att['size'])); $str.=sprintf('<a class="Icon file no-pjax" href="%s?id=%d&h=%s" target="%s">%s</a>%s %s', - $file, $attachment['attach_id'], $hash, $target, Format::htmlchars($attachment['name']), $size, $separator); + $file, + $att['attach_id'], + $att['download'], + $target, + Format::htmlchars($att['name']), + $size, + $separator); } return $str; @@ -774,12 +702,17 @@ Class ThreadEntry { /* static */ function logEmailHeaders($id, $mid, $header=false) { - $sql='INSERT INTO '.TICKET_EMAIL_INFO_TABLE - .' SET thread_id='.db_input($id) - .', email_mid='.db_input($mid); //TODO: change it to message_id. + + if (!$id || !$mid) + return false; + + $sql='INSERT INTO '.THREAD_ENTRY_EMAIL_TABLE + .' SET thread_entry_id='.db_input($id) + .', mid='.db_input($mid); if ($header) $sql .= ', headers='.db_input($header); - return db_query($sql)?db_insert_id():0; + + return db_query($sql) ? db_insert_id() : 0; } /* variables */ @@ -809,12 +742,10 @@ Class ThreadEntry { return false; } - /* static calls */ - - function lookup($id, $tid=0, $type='') { + static function lookup($id, $tid=0, $type='') { return ($id && is_numeric($id) - && ($e = new ThreadEntry($id, $type, $tid)) + && ($e = new ThreadEntry($id, $tid, $type)) && $e->getId()==$id )?$e:null; } @@ -835,8 +766,9 @@ Class ThreadEntry { function lookupByEmailHeaders(&$mailinfo, &$seen=false) { // Search for messages using the References header, then the // in-reply-to header - $search = 'SELECT thread_id, email_mid FROM '.TICKET_EMAIL_INFO_TABLE - . ' WHERE email_mid=%s ORDER BY thread_id DESC'; + $search = 'SELECT thread_entery_id, mid FROM '.THREAD_ENTRY_EMAIL_TABLE + . ' WHERE mid=%s ' + . ' ORDER BY thread_entry_id DESC'; if (list($id, $mid) = db_fetch_row(db_query( sprintf($search, db_input($mailinfo['mid']))))) { @@ -971,7 +903,7 @@ Class ThreadEntry { global $ost; $domain = md5($ost->getConfig()->getURL()); - $ticket = $this->getTicket(); + $ticket = $this->getThread()->getObject(); return sprintf('$%s$%s@%s', base64_encode(pack('V', $this->getId())), substr(md5($to . $ticket->getNumber() . $ticket->getId()), -10), @@ -980,19 +912,19 @@ Class ThreadEntry { } //new entry ... we're trusting the caller to check validity of the data. - function create($vars) { + static function create($vars) { global $cfg; //Must have... - if(!$vars['ticketId'] || !$vars['type'] || !in_array($vars['type'], array('M','R','N'))) + if (!$vars['threadId'] || !$vars['type']) return false; - if (!$vars['body'] instanceof ThreadBody) { + if (!$vars['body'] instanceof ThreadEntryBody) { if ($cfg->isHtmlThreadEnabled()) - $vars['body'] = new HtmlThreadBody($vars['body']); + $vars['body'] = new HtmlThreadEntryBody($vars['body']); else - $vars['body'] = new TextThreadBody($vars['body']); + $vars['body'] = new TextThreadEntryBody($vars['body']); } // Drop stripped images @@ -1008,7 +940,7 @@ Class ThreadEntry { } // Handle extracted embedded images (<img src="data:base64,..." />). - // The extraction has already been performed in the ThreadBody + // The extraction has already been performed in the ThreadEntryBody // class. Here they should simply be added to the attachments list if ($atts = $vars['body']->getEmbeddedHtmlImages()) { if (!is_array($vars['attachments'])) @@ -1025,22 +957,22 @@ Class ThreadEntry { if ($poster && is_object($poster)) $poster = (string) $poster; - $sql=' INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW() ' - .' ,thread_type='.db_input($vars['type']) - .' ,ticket_id='.db_input($vars['ticketId']) - .' ,title='.db_input(Format::sanitize($vars['title'], true)) - .' ,format='.db_input($vars['body']->getType()) - .' ,staff_id='.db_input($vars['staffId']) - .' ,user_id='.db_input($vars['userId']) - .' ,poster='.db_input($poster) - .' ,source='.db_input($vars['source']); + $sql=' INSERT INTO '.THREAD_ENTRY_TABLE.' SET `created` = NOW() ' + .' ,`type` = '.db_input($vars['type']) + .' ,`thread_id` = '.db_input($vars['threadId']) + .' ,`title` = '.db_input(Format::sanitize($vars['title'], true)) + .' ,`format` = '.db_input($vars['body']->getType()) + .' ,`staff_id` = '.db_input($vars['staffId']) + .' ,`user_id` = '.db_input($vars['userId']) + .' ,`poster` = '.db_input($poster) + .' ,`source` = '.db_input($vars['source']); if (!isset($vars['attachments']) || !$vars['attachments']) // Otherwise, body will be configured in a block below (after // inline attachments are saved and updated in the database) $sql.=' ,body='.db_input($body); - if(isset($vars['pid'])) + if (isset($vars['pid'])) $sql.=' ,pid='.db_input($vars['pid']); // Check if 'reply_to' is in the $vars as the previous ThreadEntry // instance. If the body of the previous message is found in the new @@ -1049,11 +981,12 @@ Class ThreadEntry { && $vars['reply_to'] instanceof ThreadEntry) $sql.=' ,pid='.db_input($vars['reply_to']->getId()); - if($vars['ip_address']) + if ($vars['ip_address']) $sql.=' ,ip_address='.db_input($vars['ip_address']); //echo $sql; - if(!db_query($sql) || !($entry=self::lookup(db_insert_id(), $vars['ticketId']))) + if (!db_query($sql) + || !($entry=self::lookup(db_insert_id(), $vars['threadId']))) return false; /************* ATTACHMENTS *****************/ @@ -1086,16 +1019,20 @@ Class ThreadEntry { 'src="cid:'.$a['key'].'"', $body); } } - $sql = 'UPDATE '.TICKET_THREAD_TABLE.' SET body='.db_input($body) + + $sql = 'UPDATE '.THREAD_ENTRY_TABLE + .' SET body='.db_input($body) .' WHERE `id`='.db_input($entry->getId()); + if (!db_query($sql) || !db_affected_rows()) return false; } // Email message id (required for all thread posts) if (!isset($vars['mid'])) - $vars['mid'] = sprintf('<%s@%s>', Misc::randCode(24), - substr(md5($cfg->getUrl()), -10)); + $vars['mid'] = sprintf('<%s@%s>', + Misc::randCode(24), substr(md5($cfg->getUrl()), -10)); + $entry->saveEmailInfo($vars); // Inline images (attached to the draft) @@ -1106,172 +1043,13 @@ Class ThreadEntry { return $entry; } - function add($vars) { - return ($entry=self::create($vars))?$entry->getId():0; + static function add($vars) { + return ($entry=self::create($vars)) ? $entry->getId() : 0; } } -/* Message - Ticket thread entry of type message */ -class Message extends ThreadEntry { - function Message($id, $ticketId=0) { - parent::ThreadEntry($id, 'M', $ticketId); - } - - function getSubject() { - return $this->getTitle(); - } - - function create($vars, &$errors) { - return self::lookup(self::add($vars, $errors)); - } - - function add($vars, &$errors) { - - if(!$vars || !is_array($vars) || !$vars['ticketId']) - $errors['err'] = __('Missing or invalid data'); - elseif(!$vars['message']) - $errors['message'] = __('Message content is required'); - - if($errors) return false; - - $vars['type'] = 'M'; - $vars['body'] = $vars['message']; - - if (!$vars['poster'] - && $vars['userId'] - && ($user = User::lookup($vars['userId']))) - $vars['poster'] = (string) $user->getName(); - - return ThreadEntry::add($vars); - } - - function lookup($id, $tid=0, $type='M') { - - return ($id - && is_numeric($id) - && ($m = new Message($id, $tid)) - && $m->getId()==$id - )?$m:null; - } - - function lastByTicketId($ticketId) { - return self::byTicketId($ticketId); - } - - function firstByTicketId($ticketId) { - return self::byTicketId($ticketId, false); - } - - function byTicketId($ticketId, $last=true) { - - $sql=' SELECT thread.id FROM '.TICKET_THREAD_TABLE.' thread ' - .' WHERE thread_type=\'M\' AND thread.ticket_id = '.db_input($ticketId) - .sprintf(' ORDER BY thread.id %s LIMIT 1', $last ? 'DESC' : 'ASC'); - - if (($res = db_query($sql)) && ($id = db_result($res))) - return Message::lookup($id); - - return null; - } -} - -/* Response - Ticket thread entry of type response */ -class Response extends ThreadEntry { - - function Response($id, $ticketId=0) { - parent::ThreadEntry($id, 'R', $ticketId); - } - - function getSubject() { - return $this->getTitle(); - } - - function getRespondent() { - return $this->getStaff(); - } - - function create($vars, &$errors) { - return self::lookup(self::add($vars, $errors)); - } - - function add($vars, &$errors) { - - if(!$vars || !is_array($vars) || !$vars['ticketId']) - $errors['err'] = __('Missing or invalid data'); - elseif(!$vars['response']) - $errors['response'] = __('Response content is required'); - - if($errors) return false; - - $vars['type'] = 'R'; - $vars['body'] = $vars['response']; - if(!$vars['pid'] && $vars['msgId']) - $vars['pid'] = $vars['msgId']; - - if (!$vars['poster'] - && $vars['staffId'] - && ($staff = Staff::lookup($vars['staffId']))) - $vars['poster'] = (string) $staff->getName(); - - return ThreadEntry::add($vars); - } - - - function lookup($id, $tid=0, $type='R') { - - return ($id - && is_numeric($id) - && ($r = new Response($id, $tid)) - && $r->getId()==$id - )?$r:null; - } -} - -/* Note - Ticket thread entry of type note (Internal Note) */ -class Note extends ThreadEntry { - - function Note($id, $ticketId=0) { - parent::ThreadEntry($id, 'N', $ticketId); - } - - function getMessage() { - return $this->getBody(); - } - - /* static */ - function create($vars, &$errors) { - return self::lookup(self::add($vars, $errors)); - } - - function add($vars, &$errors) { - - //Check required params. - if(!$vars || !is_array($vars) || !$vars['ticketId']) - $errors['err'] = __('Missing or invalid data'); - elseif(!$vars['note']) - $errors['note'] = __('Note content is required'); - - if($errors) return false; - - //TODO: use array_intersect_key when we move to php 5 to extract just what we need. - $vars['type'] = 'N'; - $vars['body'] = $vars['note']; - - return ThreadEntry::add($vars); - } - - function lookup($id, $tid=0, $type='N') { - - return ($id - && is_numeric($id) - && ($n = new Note($id, $tid)) - && $n->getId()==$id - )?$n:null; - } -} - -class ThreadBody /* extends SplString */ { +class ThreadEntryBody /* extends SplString */ { static $types = array('text', 'html'); @@ -1286,7 +1064,7 @@ class ThreadBody /* extends SplString */ { function __construct($body, $type='text', $options=array()) { $type = strtolower($type); if (!in_array($type, static::$types)) - throw new Exception("$type: Unsupported ThreadBody type"); + throw new Exception("$type: Unsupported ThreadEntryBody type"); $this->body = (string) $body; if (strlen($this->body) > 250000) { $max_packet = db_get_variable('max_allowed_packet', 'global'); @@ -1309,10 +1087,10 @@ class ThreadBody /* extends SplString */ { $conv = $this->type . ':' . strtolower($type); switch ($conv) { case 'text:html': - return new ThreadBody(sprintf('<pre>%s</pre>', + return new ThreadEntryBody(sprintf('<pre>%s</pre>', Format::htmlchars($this->body)), $type); case 'html:text': - return new ThreadBody(Format::html2text((string) $this), $type); + return new ThreadEntryBody(Format::html2text((string) $this), $type); } } @@ -1376,16 +1154,16 @@ class ThreadBody /* extends SplString */ { static function fromFormattedText($text, $format=false) { switch ($format) { case 'text': - return new TextThreadBody($text); + return new TextThreadEntryBody($text); case 'html': - return new HtmlThreadBody($text, array('strip-embedded'=>false)); + return new HtmlThreadEntryBody($text, array('strip-embedded'=>false)); default: - return new ThreadBody($text); + return new ThreadEntryBody($text); } } } -class TextThreadBody extends ThreadBody { +class TextThreadEntryBody extends ThreadEntryBody { function __construct($body, $options=array()) { parent::__construct($body, 'text', $options); } @@ -1416,7 +1194,7 @@ class TextThreadBody extends ThreadBody { return $this->display('email'); } } -class HtmlThreadBody extends ThreadBody { +class HtmlThreadEntryBody extends ThreadEntryBody { function __construct($body, $options=array()) { if (!isset($options['strip-embedded']) || $options['strip-embedded']) $body = $this->extractEmbeddedHtmlImages($body); @@ -1461,4 +1239,290 @@ class HtmlThreadBody extends ThreadBody { } } } + + +/* Message - Ticket thread entry of type message */ +class MessageThreadEntry extends ThreadEntry { + + const ENTRY_TYPE = 'M'; + + function MessageThreadEntry($id, $threadId=0) { + parent::ThreadEntry($id, $threadId, self::ENTRY_TYPE); + } + + function getSubject() { + return $this->getTitle(); + } + + static function create($vars, &$errors) { + return self::lookup(self::add($vars, $errors)); + } + + static function add($vars, &$errors) { + + if (!$vars || !is_array($vars) || !$vars['threadId']) + $errors['err'] = __('Missing or invalid data'); + elseif (!$vars['message']) + $errors['message'] = __('Message content is required'); + + if ($errors) return false; + + $vars['type'] = self::ENTRY_TYPE; + $vars['body'] = $vars['message']; + + if (!$vars['poster'] + && $vars['userId'] + && ($user = User::lookup($vars['userId']))) + $vars['poster'] = (string) $user->getName(); + + return parent::add($vars); + } + + static function lookup($id, $tid=0) { + + return ($id + && is_numeric($id) + && ($m = new MessageThreadEntry($id, $tid)) + && $m->getId()==$id + )?$m:null; + } + +} + +/* thread entry of type response */ +class ResponseThreadEntry extends ThreadEntry { + + const ENTRY_TYPE = 'R'; + + function ResponseThreadEntry($id, $threadId=0) { + parent::ThreadEntry($id, $threadId, self::ENTRY_TYPE); + } + + function getSubject() { + return $this->getTitle(); + } + + function getRespondent() { + return $this->getStaff(); + } + + static function create($vars, &$errors) { + return self::lookup(self::add($vars, $errors)); + } + + static function add($vars, &$errors) { + + if (!$vars || !is_array($vars) || !$vars['threadId']) + $errors['err'] = __('Missing or invalid data'); + elseif (!$vars['response']) + $errors['response'] = __('Response content is required'); + + if ($errors) return false; + + $vars['type'] = self::ENTRY_TYPE; + $vars['body'] = $vars['response']; + if (!$vars['pid'] && $vars['msgId']) + $vars['pid'] = $vars['msgId']; + + if (!$vars['poster'] + && $vars['staffId'] + && ($staff = Staff::lookup($vars['staffId']))) + $vars['poster'] = (string) $staff->getName(); + + return parent::add($vars); + } + + static function lookup($id, $tid=0) { + + return ($id + && is_numeric($id) + && ($r = new ResponseThreadEntry($id, $tid)) + && $r->getId()==$id + )?$r:null; + } +} + +/* Thread entry of type note (Internal Note) */ +class NoteThreadEntry extends ThreadEntry { + const ENTRY_TYPE = 'N'; + + function NoteThreadEntry($id, $threadId=0) { + parent::ThreadEntry($id, $threadId, self::ENTRY_TYPE); + } + + function getMessage() { + return $this->getBody(); + } + + static function create($vars, &$errors) { + return self::lookup(self::add($vars, $errors)); + } + + static function add($vars, &$errors) { + + //Check required params. + if (!$vars || !is_array($vars) || !$vars['threadId']) + $errors['err'] = __('Missing or invalid data'); + elseif (!$vars['note']) + $errors['note'] = __('Note content is required'); + + if ($errors) return false; + + //TODO: use array_intersect_key when we move to php 5 to extract just what we need. + $vars['type'] = self::ENTRY_TYPE; + $vars['body'] = $vars['note']; + + return parent::add($vars); + } + + static function lookup($id, $tid=0) { + + return ($id + && is_numeric($id) + && ($n = new NoteThreadEntry($id, $tid)) + && $n->getId()==$id + )?$n:null; + } +} + +// Object specific thread utils. +class ObjectThread extends Thread { + private $_entries = array(); + + function __construct($id) { + + parent::__construct($id); + + if ($this->getId()) { + $sql= ' SELECT `type`, count(DISTINCT e.id) as count ' + .' FROM '.THREAD_TABLE. ' t ' + .' INNER JOIN '.THREAD_ENTRY_TABLE. ' e ON (e.thread_id = t.id) ' + .' WHERE t.id='.db_input($this->getId()) + .' GROUP BY e.`type`'; + + if (($res=db_query($sql)) && db_num_rows($res)) { + while ($row=db_fetch_row($res)) + $this->_entries[$row[0]] = $row[1]; + } + } + } + + function getNumMessages() { + return $this->_entries[MessageThreadEntry::ENTRY_TYPE]; + } + + function getNumResponses() { + return $this->_entries[ResponseThreadEntry::ENTRY_TYPE]; + } + + function getNumNotes() { + return $this->_entries[NoteThreadEntry::ENTRY_TYPE]; + } + + function getMessages() { + return $this->getEntries(array( + 'type' => MessageThreadEntry::ENTRY_TYPE)); + } + + function getLastMessage() { + + $criteria = array( + 'type' => MessageThreadEntry::ENTRY_TYPE, + 'order' => 'DESC', + 'limit' => 1); + + return $this->getEntry($criteria); + } + + function getEntry($var) { + + if (is_numeric($var)) + $id = $var; + else { + $criteria = array_merge($var, array('limit' => 1)); + $entries = $this->getEntries($criteria); + if ($entries && $entries[0]) + $id = $entries[0]['id']; + } + + return $id ? parent::getEntry($id) : null; + } + + function getResponses() { + return $this->getEntries(array( + 'type' => ResponseThreadEntry::ENTRY_TYPE)); + } + + function getNotes() { + return $this->getEntries(array( + 'type' => NoteThreadEntry::ENTRY_TYPE)); + } + + function addNote($vars, &$errors) { + + //Add ticket Id. + $vars['threadId'] = $this->getId(); + return NoteThreadEntry::create($vars, $errors); + } + + function addMessage($vars, &$errors) { + + $vars['threadId'] = $this->getId(); + $vars['staffId'] = 0; + + return MessageThreadEntry::create($vars, $errors); + } + + function addResponse($vars, &$errors) { + + $vars['threadId'] = $this->getId(); + $vars['userId'] = 0; + + return ResponseThreadEntry::create($vars, $errors); + } + + function getVar($name) { + switch ($name) { + case 'original': + $entries = $this->getEntries(array( + 'type' => MessageThreadEntry::ENTRY_TYPE, + 'order' => 'ASC', + 'limit' => 1)); + if ($entries && $entries[0]) + return (string) $entries[0]['body']; + + break; + case 'last_message': + case 'lastmessage': + $entries = $this->getEntries(array( + 'type' => MessageThreadEntry::ENTRY_TYPE, + 'order' => 'DESC', + 'limit' => 1)); + if ($entries && $entries[0]) + return (string) $entries[0]['body']; + + break; + } + } + + static function lookup($criteria) { + + return ($criteria + && ($t= new static($criteria)) + && $t->getId() + ) ? $t : null; + } +} + +// Ticket thread class +class TicketThread extends ObjectThread { + + static function create($ticket) { + $id = is_object($ticket) ? $ticket->getId() : $ticket; + return parent::create(array( + 'object_id' => $id, + 'object_type' => ObjectModel::OBJECT_TYPE_TICKET + )); + } +} ?> diff --git a/include/class.ticket.php b/include/class.ticket.php index 8227d1142b1533cde82b53b28b522b67ba892e54..44d3d7a7dac4a5722073fe1ab521bb42333e6d70 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -32,6 +32,7 @@ include_once(INCLUDE_DIR.'class.canned.php'); require_once(INCLUDE_DIR.'class.dynamic_forms.php'); require_once(INCLUDE_DIR.'class.user.php'); require_once(INCLUDE_DIR.'class.collaborator.php'); +require_once(INCLUDE_DIR.'class.task.php'); require_once(INCLUDE_DIR.'class.faq.php'); class TicketModel extends VerySimpleModel { @@ -183,23 +184,30 @@ class Ticket { function load($id=0) { - if(!$id && !($id=$this->getId())) + if (!$id && !($id=$this->getId())) return false; - $sql='SELECT ticket.*, lock_id, dept.name as dept_name ' - .' ,count(distinct attach.attach_id) as attachments' + $sql='SELECT ticket.*, thread.id as thread_id, lock_id, dept.name as dept_name ' + .' ,count(distinct attach.id) as attachments' + .' ,count(distinct task.id) as tasks' .' FROM '.TICKET_TABLE.' ticket ' .' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.id) ' .' LEFT JOIN '.SLA_TABLE.' sla ON (ticket.sla_id=sla.id AND sla.isactive=1) ' .' LEFT JOIN '.TICKET_LOCK_TABLE.' tlock ON ( ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW()) ' - .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach - ON ( ticket.ticket_id=attach.ticket_id) ' + .' LEFT JOIN '.TASK_TABLE.' task + ON ( task.object_id = ticket.ticket_id AND task.object_type="T" ) ' + .' LEFT JOIN '.THREAD_TABLE.' thread + ON ( thread.object_id = ticket.ticket_id AND thread.object_type="T" ) ' + .' LEFT JOIN '.THREAD_ENTRY_TABLE.' entry + ON ( entry.thread_id = thread.id ) ' + .' LEFT JOIN '.ATTACHMENT_TABLE.' attach + ON ( attach.object_id = entry.id AND attach.`type` = "H") ' .' WHERE ticket.ticket_id='.db_input($id) .' GROUP BY ticket.ticket_id'; //echo $sql; - if(!($res=db_query($sql)) || !db_num_rows($res)) + if (!($res=db_query($sql)) || !db_num_rows($res)) return false; @@ -479,7 +487,7 @@ class Ticket { function getDeptName() { if(!$this->ht['dept_name'] && ($dept = $this->getDept())) - $this->ht['dept_name'] = $dept->getName(); + $this->ht['dept_name'] = $dept->getFullName(); return $this->ht['dept_name']; } @@ -657,19 +665,25 @@ class Ticket { function getLastRespondent() { - $sql ='SELECT resp.staff_id ' - .' FROM '.TICKET_THREAD_TABLE.' resp ' - .' LEFT JOIN '.STAFF_TABLE. ' USING(staff_id) ' - .' WHERE resp.ticket_id='.db_input($this->getId()).' AND resp.staff_id>0 ' - .' AND resp.thread_type="R"' - .' ORDER BY resp.created DESC LIMIT 1'; + if (!isset($this->lastrespondent)) { - if(!($res=db_query($sql)) || !db_num_rows($res)) - return null; + $sql ='SELECT resp.staff_id ' + .' FROM '.THREAD_ENTRY_TABLE.' resp ' + .' LEFT JOIN '.THREAD_TABLE.' t ON( t.id=resp.thread_id) ' + .' LEFT JOIN '.STAFF_TABLE. ' s ON(s.staff_id=resp.staff_id) ' + .' WHERE t.object_id='.db_input($this->getId()) + .' AND t.object_type="T" AND resp.staff_id>0 AND resp.`type`="R" ' + .' ORDER BY resp.created DESC LIMIT 1'; + + if(!($res=db_query($sql)) || !db_num_rows($res)) + return null; + + list($id)=db_fetch_row($res); - list($id)=db_fetch_row($res); + $this->lastrespondent = Staff::lookup($id); + } - return Staff::lookup($id); + return $this->lastrespondent; } @@ -695,21 +709,31 @@ class Ticket { } function getLastMessage() { + if (!isset($this->last_message)) { - if($this->getLastMsgId()) - $this->last_message = Message::lookup( - $this->getLastMsgId(), $this->getId()); + if ($this->getLastMsgId()) + $this->last_message = MessageThreadEntry::lookup( + $this->getLastMsgId(), $this->getThreadId()); if (!$this->last_message) - $this->last_message = Message::lastByTicketId($this->getId()); + $this->last_message = $this->getThread()->getLastMessage(); } + return $this->last_message; } + function getNumTasks() { + return $this->ht['tasks']; + } + + function getThreadId() { + return $this->ht['thread_id']; + } + function getThread() { - if(!$this->thread) - $this->thread = Thread::lookup($this); + if (!$this->thread && $this->getThreadId()) + $this->thread = TicketThread::lookup($this->getThreadId()); return $this->thread; } @@ -751,7 +775,8 @@ class Ticket { } function getThreadEntries($type, $order='') { - return $this->getThread()->getEntries($type, $order); + return $this->getThread()->getEntries( + array( 'type' => $type, 'order' => $order)); } //Collaborators @@ -1343,8 +1368,16 @@ class Ticket { if ($this->isClosed() && $this->isReopenable()) $this->reopen(); - /********** double check auto-response ************/ - if (!($user = $message->getUser())) + // Figure out the user + if ($this->getOwnerId() == $message->getUserId()) + $user = new TicketOwner( + User::lookup($message->getUserId()), $this); + else + $user = Collaborator::lookup(array( + 'userId'=>$message->getUserId(), 'ticketId'=>$this->getId())); + + /********** double check auto-response ************/ + if (!$user) $autorespond=false; elseif ($autorespond && (Email::getIdByEmail($user->getEmail()))) $autorespond=false; @@ -1930,10 +1963,10 @@ class Ticket { $files[] = $file['id']; if ($cfg->isHtmlThreadEnabled()) - $response = new HtmlThreadBody( + $response = new HtmlThreadEntryBody( $this->replaceVars($canned->getHtml())); else - $response = new TextThreadBody( + $response = new TextThreadEntryBody( $this->replaceVars($canned->getPlainText())); $info = array('msgId' => $msgId, @@ -2002,6 +2035,7 @@ class Ticket { && $cfg->autoClaimTickets()) $this->setStaffId($thisstaff->getId()); //direct assignment; + $this->lastrespondent = null; $this->onResponse(); //do house cleaning.. /* email the user?? - if disabled - then bail out */ @@ -2084,7 +2118,7 @@ class Ticket { $errors = array(); //Unless specified otherwise, assume HTML if ($note && is_string($note)) - $note = new HtmlThreadBody($note); + $note = new HtmlThreadEntryBody($note); return $this->postNote( array( @@ -2833,7 +2867,10 @@ class Ticket { $sql.=' ,duedate='.db_input(date('Y-m-d G:i',Misc::dbtime($vars['duedate'].' '.$vars['time']))); - if(!db_query($sql) || !($id=db_insert_id()) || !($ticket =Ticket::lookup($id))) + if(!db_query($sql) + || !($id=db_insert_id()) + || !($thread=TicketThread::create($id)) + || !($ticket =Ticket::lookup($id))) return null; /* -------------------- POST CREATE ------------------------ */ @@ -2994,7 +3031,7 @@ class Ticket { // post response - if any $response = null; - if($vars['response'] && $role->canPostReply()) { + if($vars['response'] && $role->canPostTicketReply()) { $vars['response'] = $ticket->replaceVars($vars['response']); // $vars['cannedatachments'] contains the attachments placed on diff --git a/include/class.translation.php b/include/class.translation.php index 141e13051c98dd709bce0a4bcc705a1aaba7751e..1c09dbb6aa29a31b63df9ed210a9dd582aa1534e 100644 --- a/include/class.translation.php +++ b/include/class.translation.php @@ -1032,29 +1032,6 @@ class CustomDataTranslation extends VerySimpleModel { return static::objects()->filter($criteria)->all(); } - - static function getDepartmentNames($ids) { - global $cfg; - - $tags = array(); - $names = array(); - foreach ($ids as $i) - $tags[_H('dept.name.'.$i)] = $i; - - if (($lang = Internationalization::getCurrentLanguage()) - && $lang != $cfg->getPrimaryLanguage() - ) { - foreach (CustomDataTranslation::objects()->filter(array( - 'object_hash__in'=>array_keys($tags), - 'lang'=>$lang - )) as $translation - ) { - $names[$tags[$translation->object_hash]] = $translation->text; - } - } - return $names; - } - } class CustomTextDomain { diff --git a/include/class.user.php b/include/class.user.php index 8e6382e9dbb04c84e4485b421b18ef47d7f9dd09..0010004372ace9b061a07a2a51fb6c6719ebc825 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -287,7 +287,7 @@ class User extends UserModel { function getDynamicData($create=true) { if (!isset($this->_entries)) { - $this->_entries = DynamicFormEntry::forClient($this->id)->all(); + $this->_entries = DynamicFormEntry::forObject($this->id, 'U')->all(); if (!$this->_entries && $create) { $g = UserForm::getNewInstance(); $g->setClientId($this->id); diff --git a/include/client/templates/dynamic-form.tmpl.php b/include/client/templates/dynamic-form.tmpl.php index 0672b2263829dea6aa804a6345c2c2489ff84c61..14bcf5432cff883ab27b256b52d650d962a5eb08 100644 --- a/include/client/templates/dynamic-form.tmpl.php +++ b/include/client/templates/dynamic-form.tmpl.php @@ -37,12 +37,12 @@ <br/> <?php } - $field->render('client'); + $field->render(array('client'=>true)); ?></label><?php foreach ($field->errors() as $e) { ?> <div class="error"><?php echo $e; ?></div> <?php } - $field->renderExtras('client'); + $field->renderExtras(array('client'=>true)); ?> </td> </tr> diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php index d0529ba3eed6a990c24420b454a0c7a738a521a6..e39e43671662b1eff01b62e89c79a9d843f1e6d8 100644 --- a/include/client/tickets.inc.php +++ b/include/client/tickets.inc.php @@ -68,6 +68,15 @@ $pageNav=new Pagenate($total, $page, PAGE_LIMIT); $pageNav->setURL('tickets.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order'])); $pageNav->paginate($tickets); +//more stuff... +$qselect.=' ,count(DISTINCT attach.id) as attachments '; +$qfrom.=' LEFT JOIN '.THREAD_ENTRY_TABLE.' entry + ON (entry.thread_id=thread.id AND entry.`type` IN ("M", "R")) '; +$qfrom.=' LEFT JOIN '.ATTACHMENT_TABLE.' attach + ON (attach.object_id=entry.id AND attach.`type` = "H") '; +$qgroup=' GROUP BY ticket.ticket_id'; + +$query="$qselect $qfrom $qwhere $qgroup ORDER BY $order_by $order LIMIT ".$pageNav->getStart().",".$pageNav->getLimit(); //echo $query; $showing =$total ? $pageNav->showing() : ""; if(!$results_type) diff --git a/include/client/view.inc.php b/include/client/view.inc.php index 2a98a5c72acd99ddf44cd9f5a0fef896ef7dc23d..4c93a7b3bcb5b21eeac0657c1ccaf84ebe133b53 100644 --- a/include/client/view.inc.php +++ b/include/client/view.inc.php @@ -114,12 +114,12 @@ if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) { foreach($thread as $entry) { //Making sure internal notes are not displayed due to backend MISTAKES! - if(!$threadType[$entry['thread_type']]) continue; + if(!$threadType[$entry['type']]) continue; $poster = $entry['poster']; - if($entry['thread_type']=='R' && ($cfg->hideStaffName() || !$entry['staff_id'])) + if($entry['type']=='R' && ($cfg->hideStaffName() || !$entry['staff_id'])) $poster = ' '; ?> - <table class="thread-entry <?php echo $threadType[$entry['thread_type']]; ?>" cellspacing="0" cellpadding="1" width="800" border="0"> + <table class="thread-entry <?php echo $threadType[$entry['type']]; ?>" cellspacing="0" cellpadding="1" width="800" border="0"> <tr><th><div> <?php echo Format::datetime($entry['created']); ?> <span class="textra"></span> @@ -184,7 +184,7 @@ if (!$ticket->isClosed() || $ticket->isReopenable()) { ?> <?php if ($messageField->isAttachmentsEnabled()) { ?> <?php - print $attachments->render(true); + print $attachments->render(array('client'=>true)); ?> <?php } ?> diff --git a/include/i18n/en_US/config.yaml b/include/i18n/en_US/config.yaml index b29df10ace86324407ba25ab5605ed7d569cdf04..113cda50d9618564e15d44b60a3602bd4d7d9cd5 100644 --- a/include/i18n/en_US/config.yaml +++ b/include/i18n/en_US/config.yaml @@ -74,8 +74,10 @@ core: hide_staff_name: 0 overlimit_notice_active: 0 email_attachments: 1 - number_format: '######' - sequence_id: 0 + ticket_number_format: '######' + ticket_sequence_id: 0 + task_number_format: '#' + task_sequence_id: 2 log_level: 2 log_graceperiod: 12 client_registration: 'public' diff --git a/include/i18n/en_US/form.yaml b/include/i18n/en_US/form.yaml index 363b9abef8642f8587fa85bf4d19854a1c68a746..7d1eb9aa2f5303d1c35b2c04c9a9b15102b1e9de 100644 --- a/include/i18n/en_US/form.yaml +++ b/include/i18n/en_US/form.yaml @@ -97,7 +97,7 @@ type: priority # notrans name: priority # notrans label: Priority Level - flags: 0x430A3 + flags: 0x430B1 sort: 3 - type: C # notrans title: Company Information @@ -181,3 +181,24 @@ configuration: rows: 4 cols: 40 +- type: A # notrans + title: Task Details + instructions: Please Describe The Issue + notes: | + This form is used to create a task. + deletable: false + fields: + - type: text # notrans + name: title # notrans + flags: 0x470B1 + sort: 1 + label: Title + configuration: + size: 40 + length: 50 + - type: thread # notrans + name: description # notrans + flags: 0x450B3 + sort: 2 + label: Description + hint: Details on the reason(s) for creating the task. diff --git a/include/i18n/en_US/role.yaml b/include/i18n/en_US/role.yaml index f3def31e27e96f965ff5aa4fc8c3325ea225d81c..d83a60c2914848730123c02c623b52e7b210cd71 100644 --- a/include/i18n/en_US/role.yaml +++ b/include/i18n/en_US/role.yaml @@ -24,6 +24,13 @@ ticket.reply, ticket.close, ticket.delete, + task.create, + task.edit, + task.assign, + task.transfer, + task.reply, + task.close, + task.delete, kb.premade, kb.faq, stats.agents, @@ -42,6 +49,12 @@ ticket.transfer, ticket.reply, ticket.close, + task.create, + task.edit, + task.assign, + task.transfer, + task.reply, + task.close, kb.premade, kb.faq, stats.agents, @@ -57,4 +70,8 @@ ticket.create, ticket.assign, ticket.transfer, - ticket.reply] + ticket.reply + task.create, + task.assign, + task.transfer, + task.reply] diff --git a/include/i18n/en_US/sequence.yaml b/include/i18n/en_US/sequence.yaml index bb502c3526c287280a2fe664419989e26c9307ef..f67594aa91269309ddad0a470c3a3c07ac85e151 100644 --- a/include/i18n/en_US/sequence.yaml +++ b/include/i18n/en_US/sequence.yaml @@ -21,3 +21,10 @@ padding: '0' increment: 1 flags: 1 + +- id: 2 + name: "Tasks Sequence" + next: 1 + padding: '0' + increment: 1 + flags: 1 diff --git a/include/staff/department.inc.php b/include/staff/department.inc.php index 6828b06925d9bdee216ed73825707b5e8eafeae7..9b6d9d4636ef0740e6f13d5464402d6152b06fae 100644 --- a/include/staff/department.inc.php +++ b/include/staff/department.inc.php @@ -51,6 +51,23 @@ $info = Format::htmlchars(($errors && $_POST) ? $_POST : $info); </tr> </thead> <tbody> + <tr> + <td width="180"> + <?php echo __('Parent');?>: + </td> + <td> + <select name="pid"> + <option value="">— <?php echo __('Top-Level Deptartment'); ?> —</option> +<?php foreach (Dept::getDepartments() as $id=>$name) { + if ($info['id'] && $id == $info['id']) + continue; ?> + <option value="<?php echo $id; ?>" <?php + if ($info['pid'] == $id) echo 'selected="selected"'; + ?>><?php echo $name; ?></option> +<?php } ?> + </select> + </td> + </tr> <tr> <td width="180" class="required"> <?php echo __('Name');?>: diff --git a/include/staff/departments.inc.php b/include/staff/departments.inc.php index 20018b8dabde7baa4df3475a0951cd8681dca5f6..4b58fea69028f9636061a7cde8a041442cd24f35 100644 --- a/include/staff/departments.inc.php +++ b/include/staff/departments.inc.php @@ -100,7 +100,7 @@ $qstr.='&order='.($order=='DESC'?'ASC':'DESC'); <?php echo $default? 'disabled="disabled"' : ''; ?> > </td> <td><a href="departments.php?id=<?php echo $id; ?>"><?php - echo $dept->getName(); ?></a> <?php echo $default; ?></td> + echo Dept::getNameById($id); ?></a> <?php echo $default; ?></td> <td><?php echo $dept->isPublic() ? __('Public') :'<b>'.__('Private').'</b>'; ?></td> <td> <b> diff --git a/include/staff/directory.inc.php b/include/staff/directory.inc.php index d57967fc033f8fabbb87f1657f598515f5acfe27..74269f6ce83cc97aa921b83245400f0ac15f5d66 100644 --- a/include/staff/directory.inc.php +++ b/include/staff/directory.inc.php @@ -3,7 +3,7 @@ if(!defined('OSTSTAFFINC') || !$thisstaff || !$thisstaff->isStaff()) die('Access $qstr=''; $select='SELECT staff.*,CONCAT_WS(" ",firstname,lastname) as name,dept.name as dept '; $from='FROM '.STAFF_TABLE.' staff '. - 'LEFT JOIN '.DEPT_TABLE.' dept ON(staff.dept_id=dept.dept_id) '; + 'LEFT JOIN '.DEPT_TABLE.' dept ON(staff.dept_id=dept.id) '; $where='WHERE staff.isvisible=1 '; $agents = Staff::objects() diff --git a/include/staff/dynamic-forms.inc.php b/include/staff/dynamic-forms.inc.php index bfa399490f858aebbc46bdf25354d010be718a2e..55f1fc04fb0404c63618a19da932994774ef4f1c 100644 --- a/include/staff/dynamic-forms.inc.php +++ b/include/staff/dynamic-forms.inc.php @@ -31,6 +31,7 @@ $showing=$pageNav->showing().' '._N('form','forms',$count); $forms = array( 'U' => 'icon-user', 'T' => 'icon-ticket', + 'A' => 'icon-tasks', 'C' => 'icon-building', 'O' => 'icon-group', ); diff --git a/include/staff/dynamic-list.inc.php b/include/staff/dynamic-list.inc.php index fbeb002087925034a5e08ad7f6135e3b979af9c2..c0e009bc46984267efebd989a398ebc3d7701eba 100644 --- a/include/staff/dynamic-list.inc.php +++ b/include/staff/dynamic-list.inc.php @@ -27,7 +27,7 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info) <h2><?php echo __('Custom List'); ?> <?php echo $list ? $list->getName() : 'Add new list'; ?></h2> -<ul class="tabs"> +<ul class="tabs" id="list-tabs"> <li class="active"><a href="#definition"> <i class="icon-plus"></i> <?php echo __('Definition'); ?></a></li> <li><a href="#items"> @@ -35,7 +35,7 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info) <li><a href="#properties"> <i class="icon-asterisk"></i> <?php echo __('Properties'); ?></a></li> </ul> - +<div id="list-tabs_container"> <div id="definition" class="tab_content"> <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> <thead> @@ -108,7 +108,7 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info) </tbody> </table> </div> -<div id="properties" class="tab_content" style="display:none"> +<div id="properties" class="hidden tab_content"> <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> <thead> <tr> @@ -211,7 +211,7 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info) </tbody> </table> </div> -<div id="items" class="tab_content" style="display:none"> +<div id="items" class="hidden tab_content"> <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> <thead> <?php if ($list) { @@ -331,6 +331,7 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info) </tbody> </table> </div> +</div> <p class="centered"> <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> <input type="reset" name="reset" value="<?php echo __('Reset'); ?>"> diff --git a/include/staff/footer.inc.php b/include/staff/footer.inc.php index 2c0c45eb0d6b76c73317d26612dde1371ea68e63..5abc0997369e1e00e9e3d79bc98ae92b671438ca 100644 --- a/include/staff/footer.inc.php +++ b/include/staff/footer.inc.php @@ -19,7 +19,7 @@ if(is_object($thisstaff) && $thisstaff->isStaff()) { ?> <i class="icon-spinner icon-spin icon-3x pull-left icon-light"></i> <h1><?php echo __('Loading ...');?></h1> </div> -<div class="dialog draggable" style="display:none;width:650px;" id="popup"> +<div class="dialog draggable" style="display:none;" id="popup"> <div id="popup-loading"> <h1 style="margin-bottom: 20px;"><i class="icon-spinner icon-spin icon-large"></i> <?php echo __('Loading ...');?></h1> diff --git a/include/staff/helptopic.inc.php b/include/staff/helptopic.inc.php index 21dd951f3fa1d6f1ab48343775ab53f79b1c688c..ee8616919d13d61a0b3f38d6db876f93011115f0 100644 --- a/include/staff/helptopic.inc.php +++ b/include/staff/helptopic.inc.php @@ -118,12 +118,9 @@ if ($info['form_id'] == Topic::FORM_USE_PARENT) echo 'selected="selected"'; <select name="dept_id"> <option value="0">— <?php echo __('System Default'); ?> —</option> <?php - if (($depts=Dept::getDepartments())) { - foreach ($depts as $id => $name) { - $selected=($info['dept_id'] && $id==$info['dept_id'])?'selected="selected"':''; - echo sprintf('<option value="%d" %s>%s</option>', - $id, $selected, $name); - } + foreach (Dept::getDepartments() as $id=>$name) { + $selected=($info['dept_id'] && $id==$info['dept_id'])?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name); } ?> </select> diff --git a/include/staff/org-view.inc.php b/include/staff/org-view.inc.php index 06119767db764a3110a46a224aa3cf136186d811..112570e2e8c0f7679281b1d0a2cb88dbbfb31f06 100644 --- a/include/staff/org-view.inc.php +++ b/include/staff/org-view.inc.php @@ -63,32 +63,34 @@ if(!defined('OSTSCPINC') || !$thisstaff || !is_object($org)) die('Invalid path') </table> <br> <div class="clear"></div> -<ul class="tabs"> - <li class="active"><a id="users_tab" href="#users"><i +<ul class="tabs" id="orgtabs"> + <li class="active"><a href="#users"><i class="icon-user"></i> <?php echo __('Users'); ?></a></li> - <li><a id="tickets_tab" href="#tickets"><i + <li><a href="#tickets"><i class="icon-list-alt"></i> <?php echo __('Tickets'); ?></a></li> - <li><a id="notes_tab" href="#notes"><i + <li><a href="#notes"><i class="icon-pushpin"></i> <?php echo __('Notes'); ?></a></li> </ul> +<div id="orgtabs_container"> <div class="tab_content" id="users"> <?php include STAFFINC_DIR . 'templates/users.tmpl.php'; ?> </div> -<div class="tab_content" id="tickets" style="display:none;"> +<div class="hidden tab_content" id="tickets"> <?php include STAFFINC_DIR . 'templates/tickets.tmpl.php'; ?> </div> -<div class="tab_content" id="notes" style="display:none"> +<div class="hidden tab_content" id="notes"> <?php $notes = QuickNote::forOrganization($org); $create_note_url = 'orgs/'.$org->getId().'/note'; include STAFFINC_DIR . 'templates/notes.tmpl.php'; ?> </div> +</div> <script type="text/javascript"> $(function() { diff --git a/include/staff/templates/dynamic-form.tmpl.php b/include/staff/templates/dynamic-form.tmpl.php index b0a3d7d47457eaffb4bc2998a4994ae0ca557aef..487ce9a56f863a735abe6808f29a95c56bf2dc9f 100644 --- a/include/staff/templates/dynamic-form.tmpl.php +++ b/include/staff/templates/dynamic-form.tmpl.php @@ -56,7 +56,7 @@ if (isset($options['entry']) && $options['mode'] == 'edit') { ?> <?php echo Format::htmlchars($field->getLocal('label')); ?>:</td> <td><div style="position:relative"><?php } - $field->render(); ?> + $field->render($options); ?> <?php if (!$field->isBlockLevel() && $field->isRequiredForStaff()) { ?> <span class="error">*</span> <?php diff --git a/include/staff/templates/list-item-properties.tmpl.php b/include/staff/templates/list-item-properties.tmpl.php index df6487fd814857d2ffac204829a59110b4a57b03..def77747a2bea226a3e73bece150c380aea696bd 100644 --- a/include/staff/templates/list-item-properties.tmpl.php +++ b/include/staff/templates/list-item-properties.tmpl.php @@ -29,7 +29,7 @@ </div><div> <?php if ($internal && !$f->isEditable()) - $f->render('view'); + $f->render(array('mode'=>'view')); else { $f->render(); if ($f->get('required')) { ?> diff --git a/include/staff/templates/org-profile.tmpl.php b/include/staff/templates/org-profile.tmpl.php index 68819629cbb3d01a142ba1e68f6bdea4feda2975..c73a4f5a34d503a05a2ff8a2c2a5394bcdf1fdd7 100644 --- a/include/staff/templates/org-profile.tmpl.php +++ b/include/staff/templates/org-profile.tmpl.php @@ -13,16 +13,16 @@ if ($info['error']) { } elseif ($info['msg']) { echo sprintf('<p id="msg_notice">%s</p>', $info['msg']); } ?> -<ul class="tabs"> - <li class="active"><a href="#tab-profile" +<ul class="tabs" id="orgprofile"> + <li class="active"><a href="#profile" ><i class="icon-edit"></i> <?php echo __('Fields'); ?></a></li> <li><a href="#contact-settings" ><i class="icon-fixed-width icon-cogs faded"></i> <?php echo __('Settings'); ?></a></li> </ul> <form method="post" class="org" action="<?php echo $action; ?>"> - -<div class="tab_content" id="tab-profile" style="margin:5px;"> +<div id="orgprofile_container"> +<div class="tab_content" id="profile" style="margin:5px;"> <?php $action = $info['action'] ? $info['action'] : ('#orgs/'.$org->getId()); if ($ticket && $ticket->getOwnerId() == $user->getId()) @@ -38,7 +38,7 @@ if ($ticket && $ticket->getOwnerId() == $user->getId()) </table> </div> -<div class="tab_content" id="contact-settings" style="display:none;margin:5px;"> +<div class="hidden tab_content" id="contact-settings" style="margin:5px;"> <table style="width:100%"> <tbody> <tr> @@ -139,7 +139,7 @@ if ($ticket && $ticket->getOwnerId() == $user->getId()) </tbody> </table> </div> - +</div> <div class="clear"></div> <hr> diff --git a/include/staff/templates/sequence-manage.tmpl.php b/include/staff/templates/sequence-manage.tmpl.php index 6827cd6267d5d5f0c8cbae54a0af44cf162c91be..10ac7391ba63dba74a9f07ccb21fbe15e2dfba58 100644 --- a/include/staff/templates/sequence-manage.tmpl.php +++ b/include/staff/templates/sequence-manage.tmpl.php @@ -20,7 +20,7 @@ foreach ($sequences as $e) { <i class="icon-sort-by-order"></i> <div style="display:inline-block" class="name"> <?php echo $e->getName(); ?> </div> <div class="manage-buttons pull-right"> - <span class="faded">next</span> + <span class="faded"><?php echo __('next'); ?></span> <span class="current"><?php echo $e->current(); ?></span> </div> <div class="button-group"> diff --git a/include/staff/templates/task-assign.tmpl.php b/include/staff/templates/task-assign.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..0c5dd7a5eac411e9f78ab99e0096a0dd671187a8 --- /dev/null +++ b/include/staff/templates/task-assign.tmpl.php @@ -0,0 +1,95 @@ +<?php +global $cfg; + +if (!$info['title']) + $info['title'] = sprintf(__('%s Tasks #%s'), + $task->isAssigned() ? __('Reassign') : __('Assign'), + $task->getNumber() + ); + +?> +<h3><?php echo $info['title']; ?></h3> +<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b> +<div class="clear"></div> +<hr/> +<?php +if ($info['error']) { + echo sprintf('<p id="msg_error">%s</p>', $info['error']); +} elseif ($info['warn']) { + echo sprintf('<p id="msg_warning">%s</p>', $info['warn']); +} elseif ($info['msg']) { + echo sprintf('<p id="msg_notice">%s</p>', $info['msg']); +} elseif ($info['notice']) { + echo sprintf('<p id="msg_info"><i class="icon-info-sign"></i> %s</p>', + $info['notice']); +} + + +$action = $info['action'] ?: ('#tasks/'.$task->getId().'/assign'); +?> +<div id="ticket-status" style="display:block; margin:5px;"> +<form method="post" name="transfer" id="transfer" + action="<?php echo $action; ?>"> + <table width="100%"> + <?php + if ($info['extra']) { + ?> + <tbody> + <tr><td colspan="2"><strong><?php echo $info['extra']; + ?></strong></td> </tr> + </tbody> + <?php + } + ?> + <tbody> + <tr><td colspan=2> + <span> + <strong><?php echo __('Agent') ?>: </strong> + <select name="staff_id"> + <?php + foreach (Staff::getAvailableStaffMembers() as $id => $name) { + echo sprintf('<option value="%d" %s>%s</option>', + $id, + ($info['staff_id'] == $id) + ? 'selected="selected"' : '', + $name + ); + } + ?> + </select> + <font class="error">* <?php echo + $errors['dept_id']; ?></font> + </span> + </td> </tr> + </tbody> + <tbody> + <tr> + <td colspan="2"> + <?php + $placeholder = $info['placeholder'] ?: __('Optional reason for the assignment'); + ?> + <textarea name="comments" id="comments" + cols="50" rows="3" wrap="soft" style="width:100%" + class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext'; + ?> no-bar" + placeholder="<?php echo $placeholder; ?>"><?php + echo $info['comments']; ?></textarea> + </td> + </tr> + </tbody> + </table> + <hr> + <p class="full-width"> + <span class="buttons pull-left"> + <input type="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 $verb ?: __('Submit'); ?>"> + </span> + </p> +</form> +</div> +<div class="clear"></div> diff --git a/include/staff/templates/task-delete.tmpl.php b/include/staff/templates/task-delete.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..6014bd88aa6be37d1027d01cbb083ba6b4d03b1b --- /dev/null +++ b/include/staff/templates/task-delete.tmpl.php @@ -0,0 +1,74 @@ +<?php +global $cfg; + +if (!$info['title']) + $info['title'] = sprintf(__('%s Tasks #%s'), + __('Delete'), + $task->getNumber() + ); + +?> +<h3><?php echo $info['title']; ?></h3> +<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b> +<div class="clear"></div> +<hr/> +<?php +if ($info['error']) { + echo sprintf('<p id="msg_error">%s</p>', $info['error']); +} elseif ($info['warn']) { + echo sprintf('<p id="msg_warning">%s</p>', $info['warn']); +} elseif ($info['msg']) { + echo sprintf('<p id="msg_notice">%s</p>', $info['msg']); +} elseif ($info['notice']) { + echo sprintf('<p id="msg_info"><i class="icon-info-sign"></i> %s</p>', + $info['notice']); +} + + +$action = $info['action'] ?: ('#tasks/'.$task->getId().'/delete'); +?> +<div id="ticket-status" style="display:block; margin:5px;"> +<form method="post" name="delete" id="delete" + action="<?php echo $action; ?>"> + <table width="100%"> + <?php + if ($info['extra']) { + ?> + <tbody> + <tr><td colspan="2"><strong><?php echo $info['extra']; + ?></strong></td> </tr> + </tbody> + <?php + } + ?> + <tbody> + <tr> + <td colspan="2"> + <?php + $placeholder = $info['placeholder'] ?: __('Optional reason for the deletion'); + ?> + <textarea name="comments" id="comments" + cols="50" rows="3" wrap="soft" style="width:100%" + class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext'; + ?> no-bar" + placeholder="<?php echo $placeholder; ?>"><?php + echo $info['comments']; ?></textarea> + </td> + </tr> + </tbody> + </table> + <hr> + <p class="full-width"> + <span class="buttons pull-left"> + <input type="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 $verb ?: __('Submit'); ?>"> + </span> + </p> +</form> +</div> +<div class="clear"></div> diff --git a/include/staff/templates/task-edit.tmpl.php b/include/staff/templates/task-edit.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..a77079e251be82ab85d584083a05a540fe2eb977 --- /dev/null +++ b/include/staff/templates/task-edit.tmpl.php @@ -0,0 +1,96 @@ +<?php +global $cfg; + +if (!$info['title']) + $info['title'] = sprintf(__('%s Tasks #%s'), + __('Edit'), $task->getNumber() + ); + +$action = $info['action'] ?: ('#tasks/'.$task->getId().'/edit'); + +?> +<div id="task-form"> +<h3><?php echo $info['title']; ?></h3> +<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b> +<hr/> +<?php + +if ($info['error']) { + echo sprintf('<p id="msg_error">%s</p>', $info['error']); +} elseif ($info['warning']) { + echo sprintf('<p id="msg_warning">%s</p>', $info['warning']); +} elseif ($info['msg']) { + echo sprintf('<p id="msg_notice">%s</p>', $info['msg']); +} ?> +<div id="edit-task-form" style="display:block;"> +<form method="post" class="task" action="<?php echo $action; ?>"> + + <table class="form_table dynamic-forms" width="100%" border="0" cellspacing="0" cellpadding="2"> + <?php if ($forms) + foreach ($forms as $form) { + $form->render(true, false, array('mode'=>'edit','width'=>160,'entry'=>$form)); + print $form->getForm()->getMedia(); + } ?> + </table> + <table class="form_table dynamic-forms" width="100%" border="0" cellspacing="0" cellpadding="2"> + <tr><th colspan=2><em><?php + echo __('Task Visibility & Assignment'); ?></em></th></tr> + <?php + $iform = $iform ?: TaskForm::getInternalForm(); + foreach ($iform->getFields() as $name=>$field) { + if (!$field->isEditable()) continue; + ?> + <tr> + <td class="multi-line <?php if ($field->get('required')) echo 'required'; + ?>" style="min-width:120px;" width="160"> + <?php echo Format::htmlchars($field->get('label')); ?>:</td> + <td> + <fieldset id="field<?php echo $field->getWidget()->id; + ?>" <?php if (!$field->isVisible()) echo 'style="display:none;"'; ?>> + <?php echo $field->render(); ?> + <?php if ($field->get('required')) { ?> + <span class="error">*</span> + <?php + } + foreach ($field->errors() as $E) { + ?><div class="error"><?php echo $E; ?></div><?php + } ?> + </fieldset> + </td> + </tr> + <?php + } + ?> + </table> + <table class="form_table" width="100%" border="0" cellspacing="0" cellpadding="2"> + <tbody> + <tr> + <th colspan="2"> + <em><strong><?php echo __('Internal Note');?></strong>: <?php + echo __('Reason for editing the task (optional');?> <font class="error"> <?php echo $errors['note'];?></font></em> + </th> + </tr> + <tr> + <td colspan="2"> + <textarea class="richtext no-bar" name="note" cols="21" + rows="6" style="width:80%;"><?php echo $info['note']; + ?></textarea> + </td> + </tr> + </tbody> + </table> + <hr> + <p class="full-width"> + <span class="buttons pull-left"> + <input type="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 __('Update'); ?>"> + </span> + </p> +</form> +</div> +<div class="clear"></div> +</div> diff --git a/include/staff/templates/task-preview.tmpl.php b/include/staff/templates/task-preview.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..d6968f49a42da80daa6ccc8f730d548edbd519b0 --- /dev/null +++ b/include/staff/templates/task-preview.tmpl.php @@ -0,0 +1,80 @@ +<?php +$error=$msg=$warn=null; + +if($task->isOverdue()) + $warn.=' <span class="Icon overdueTicket">'.__('Marked overdue!').'</span>'; + +echo sprintf( + '<div style="width:600px; padding: 2px 2px 0 5px;" id="t%s"> + <h2>'.__('Task #%s').': %s</h2><br>', + $task->getNumber(), + $task->getNumber(), + Format::htmlchars($task->getTitle())); + +if($error) + echo sprintf('<div id="msg_error">%s</div>',$error); +elseif($msg) + echo sprintf('<div id="msg_notice">%s</div>',$msg); +elseif($warn) + echo sprintf('<div id="msg_warning">%s</div>',$warn); + +echo '<ul class="tabs" id="task-preview">'; + +echo ' + <li class="active"><a href="#summary" + ><i class="icon-list-alt"></i> '.__('Task Summary').'</a></li>'; +echo '</ul>'; +echo '<div id="task-preview_container">'; +echo '<div class="tab_content" id="summary">'; +echo '<table border="0" cellspacing="" cellpadding="1" width="100%" class="ticket_info">'; +$status=sprintf('<span>%s</span>',ucfirst($task->getStatus())); +echo sprintf(' + <tr> + <th width="100">'.__('Status').':</th> + <td>%s</td> + </tr> + <tr> + <th>'.__('Created').':</th> + <td>%s</td> + </tr>',$status, + Format::datetime($task->getCreateDate())); + +if ($task->isOpen() && $task->duedate) { + echo sprintf(' + <tr> + <th>'.__('Due Date').':</th> + <td>%s</td> + </tr>', + Format::datetime($task->duedate)); +} +echo '</table>'; + + +echo '<hr> + <table border="0" cellspacing="" cellpadding="1" width="100%" class="ticket_info">'; +if ($task->isOpen()) { + echo sprintf(' + <tr> + <th width="100">'.__('Assigned To').':</th> + <td>%s</td> + </tr>', $task->getAssigned() ?: ' <span class="faded">— '.__('Unassigned').' —</span>'); +} +echo sprintf( + ' + <tr> + <th width="100">'.__('Department').':</th> + <td>%s</td> + </tr>', + Format::htmlchars($task->dept->getName()) + ); + +echo ' + </table>'; +echo '</div>'; +?> +</div> +<?php +//TODO: add link to view if the user has permission + +echo '</div>'; +?> diff --git a/include/staff/templates/task-transfer.tmpl.php b/include/staff/templates/task-transfer.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..8a872f43845ec328c92554f32125e10682173b89 --- /dev/null +++ b/include/staff/templates/task-transfer.tmpl.php @@ -0,0 +1,93 @@ +<?php +global $cfg; + +if (!$info['title']) + $info['title'] = sprintf(__('%s Tasks #%s'), + __('Tranfer'), $task->getNumber()); + +?> +<h3><?php echo $info['title']; ?></h3> +<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b> +<div class="clear"></div> +<hr/> +<?php +if ($info['error']) { + echo sprintf('<p id="msg_error">%s</p>', $info['error']); +} elseif ($info['warn']) { + echo sprintf('<p id="msg_warning">%s</p>', $info['warn']); +} elseif ($info['msg']) { + echo sprintf('<p id="msg_notice">%s</p>', $info['msg']); +} elseif ($info['notice']) { + echo sprintf('<p id="msg_info"><i class="icon-info-sign"></i> %s</p>', + $info['notice']); +} + + +$action = $info['action'] ?: ('#tasks/'.$task->getId().'/transfer'); +?> +<div style="display:block; margin:5px;"> +<form method="post" name="transfer" id="transfer" + action="<?php echo $action; ?>"> + <table width="100%"> + <?php + if ($info['extra']) { + ?> + <tbody> + <tr><td colspan="2"><strong><?php echo $info['extra']; + ?></strong></td> </tr> + </tbody> + <?php + } + ?> + <tbody> + <tr><td colspan=2> + <span> + <strong><?php echo __('Department') ?>: </strong> + <select name="dept_id"> + <?php + foreach (Dept::getDepartments() as $id => $name) { + echo sprintf('<option value="%d" %s>%s</option>', + $id, + ($info['dept_id'] == $id) + ? 'selected="selected"' : '', + $name + ); + } + ?> + </select> + <font class="error">* <?php echo + $errors['dept_id']; ?></font> + </span> + </td> </tr> + </tbody> + <tbody> + <tr> + <td colspan="2"> + <?php + $placeholder = $info['placeholder'] ?: __('Optional reason for the transfer'); + ?> + <textarea name="comments" id="comments" + cols="50" rows="3" wrap="soft" style="width:100%" + class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext'; + ?> no-bar" + placeholder="<?php echo $placeholder; ?>"><?php + echo $info['comments']; ?></textarea> + </td> + </tr> + </tbody> + </table> + <hr> + <p class="full-width"> + <span class="buttons pull-left"> + <input type="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 $verb ?: __('Submit'); ?>"> + </span> + </p> +</form> +</div> +<div class="clear"></div> diff --git a/include/staff/templates/task-view.tmpl.php b/include/staff/templates/task-view.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..217afdc8c20edd5e7652d7d1b5f9be75fd2db96d --- /dev/null +++ b/include/staff/templates/task-view.tmpl.php @@ -0,0 +1,356 @@ +<?php +if (!defined('OSTSCPINC') || !$thisstaff || !is_object($task)) + die('Invalid path'); + +//Make sure the staff is allowed to access this task +/* + if (!@$thisstaff->isStaff() || !$task->checkStaffAccess($thisstaff)) + die('Access Denied'); +*/ + +$actions = array(); +$actions += array( + 'edit' => array( + 'icon' => 'icon-edit', + 'dialog' => '{"size":"large"}', + 'action' => __('Edit') + )); +$actions += array( + 'assign' => array( + 'icon' => 'icon-user', + 'action' => $task->isAssigned() ? __('Reassign') : __('Assign') + )); +$actions += array( + 'transfer' => array( + 'icon' => 'icon-share', + 'action' => __('Transfer') + )); +$actions += array( + 'delete' => array( + 'icon' => 'icon-trash', + 'action' => __('Delete') + )); + + +$info=($_POST && $errors)?Format::input($_POST):array(); + +$id = $task->getId(); //Ticket ID. +if ($task->isOverdue()) + $warn.=' <span class="Icon overdueTicket">'.__('Marked overdue!').'</span>'; + +?> +<table width="940" cellpadding="2" cellspacing="0" border="0"> + <tr> + <td width="20%" class="has_bottom_border"> + <h3><a href="#tasks/<?php echo $task->getId(); ?>/view" + id="reload-task"><i class="icon-refresh"></i> <?php + echo sprintf(__('Task #%s'), $task->getNumber()); ?></a> + </h3> + </td> + <td width="auto" class="flush-right has_bottom_border"> + <?php + if ($actions) { ?> + <span + class="action-button" + data-dropdown="#action-dropdown-taskoptions"> + <i class="icon-caret-down pull-right"></i> + <a class="task-action" + href="#taskoptions"><i + class="icon-reorder"></i> <?php + echo __('Task Options'); ?></a> + </span> + <div id="action-dropdown-taskoptions" + class="action-dropdown anchor-right"> + <ul> + <?php foreach ($actions as $a => $action) { ?> + <li> + <a class="no-pjax task-action" + <?php + if ($action['dialog']) + echo sprintf("data-dialog='%s'", $action['dialog']); + ?> + href="<?php + echo sprintf('#tasks/%d/%s', $task->getId(), $a); ?>" + ><i class="<?php + echo $action['icon'] ?: 'icon-tag'; ?>"></i> <?php + echo $action['action']; ?></a> + </li> + <?php + } ?> + </ul> + </div> + <?php + } ?> + </td> + </tr> +</table> +<table class="ticket_info" cellspacing="0" cellpadding="0" width="940" border="0"> + <tr> + <td width="50%"> + <table border="0" cellspacing="" cellpadding="4" width="100%"> + <tr> + <th width="100"><?php echo __('Status');?>:</th> + <td><?php echo $task->getStatus(); ?></td> + </tr> + <tr> + <th><?php echo __('Department');?>:</th> + <td><?php echo Format::htmlchars($task->dept->getName()); ?></td> + </tr> + <tr> + <th><?php echo __('Create Date');?>:</th> + <td><?php echo Format::datetime($task->getCreateDate()); ?></td> + </tr> + </table> + </td> + <td width="50%" style="vertical-align:top"> + <table cellspacing="0" cellpadding="4" width="100%" border="0"> + <?php + if ($task->isOpen()) { ?> + <tr> + <th width="100"><?php echo __('Assigned To');?>:</th> + <td> + <?php + if ($assigned=$task->getAssigned()) + echo Format::htmlchars($assigned); + else + echo '<span class="faded">— '.__('Unassigned').' —</span>'; + ?> + </td> + </tr> + <?php + } else { ?> + <tr> + <th width="100"><?php echo __('Closed By');?>:</th> + <td> + <?php + if (0 && ($staff = $task->getStaff())) + echo Format::htmlchars($staff->getName()); + else + echo '<span class="faded">— '.__('Unknown').' —</span>'; + ?> + </td> + </tr> + <?php + } ?> + <tr> + <th><?php echo __('SLA Plan');?>:</th> + <td><?php echo $sla?Format::htmlchars($sla->getName()):'<span class="faded">— '.__('None').' —</span>'; ?></td> + </tr> + <?php + if($task->isOpen()){ ?> + <tr> + <th><?php echo __('Due Date');?>:</th> + <td><?php echo $task->duedate ? + Format::datetime($task->duedate) : '<span + class="faded">— '.__('None').' —</span>'; ?></td> + </tr> + <?php + }else { ?> + <tr> + <th><?php echo __('Close Date');?>:</th> + <td><?php echo 0 ? + Format::datetime($task->getCloseDate()) : ''; ?></td> + </tr> + <?php + } + ?> + </table> + </td> + </tr> +</table> +<br> +<br> +<table class="ticket_info" cellspacing="0" cellpadding="0" width="940" border="0"> +<?php +$idx = 0; +foreach (DynamicFormEntry::forObject($task->getId(), + ObjectModel::OBJECT_TYPE_TASK) as $form) { + $answers = array_filter($form->getAnswers(), function ($a) { + return $a->getField()->isStorable(); + }); + if (count($answers) == 0) + continue; + ?> + <tr> + <td colspan="2"> + <table cellspacing="0" cellpadding="4" width="100%" border="0"> + <?php foreach($answers as $a) { + if (!($v = $a->display())) continue; ?> + <tr> + <th width="100"><?php + echo $a->getField()->get('label'); + ?>:</th> + <td><?php + echo $v; + ?></td> + </tr> + <?php + } ?> + </table> + </td> + </tr> + <?php + $idx++; +} ?> +</table> +<div class="clear"></div> +<div id="task_thread_container"> + <div id="task_thread_content"> + <?php + $threadTypes=array('M'=>'message','R'=>'response', 'N'=>'note'); + /* -------- Messages & Responses & Notes (if inline)-------------*/ + $types = array('M', 'R', 'N'); + if(($thread=$task->getThreadEntries($types))) { + foreach($thread as $entry) { ?> + <table class="thread-entry <?php echo $threadTypes[$entry['type']]; ?>" cellspacing="0" cellpadding="1" width="940" border="0"> + <tr> + <th colspan="4" width="100%"> + <div> + <span class="pull-left"> + <span style="display:inline-block"><?php + echo Format::datetime($entry['created']);?></span> + <span style="display:inline-block;padding:0 1em" class="faded title"><?php + echo Format::truncate($entry['title'], 100); ?></span> + </span> + <span class="pull-right" style="white-space:no-wrap;display:inline-block"> + <span style="vertical-align:middle;" class="textra"></span> + <span style="vertical-align:middle;" + class="tmeta faded title"><?php + echo Format::htmlchars($entry['name'] ?: $entry['poster']); ?></span> + </span> + </div> + </th> + </tr> + <tr><td colspan="4" class="thread-body" id="thread-id-<?php + echo $entry['id']; ?>"><div><?php + echo $entry['body']->toHtml(); ?></div></td></tr> + <?php + $urls = null; + if($entry['attachments'] + && ($tentry = $task->getThreadEntry($entry['id'])) + && ($urls = $tentry->getAttachmentUrls()) + && ($links = $tentry->getAttachmentsLinks())) {?> + <tr> + <td class="info" colspan="4"><?php echo $links; ?></td> + </tr> <?php + } + if ($urls) { ?> + <script type="text/javascript"> + $('#thread-id-<?php echo $entry['id']; ?>') + .data('urls', <?php + echo JsonDataEncoder::encode($urls); ?>) + .data('id', <?php echo $entry['id']; ?>); + </script> +<?php + } ?> + </table> + <?php + if ($entry['type'] == 'M') + $msgId = $entry['id']; + } + } else { + echo '<p>'.__('Error fetching thread - get technical help.').'</p>'; + }?> + </div> +</div> +<div class="clear" style="padding-bottom:10px;"></div> +<?php if($errors['err']) { ?> + <div id="msg_error"><?php echo $errors['err']; ?></div> +<?php }elseif($msg) { ?> + <div id="msg_notice"><?php echo $msg; ?></div> +<?php }elseif($warn) { ?> + <div id="msg_warning"><?php echo $warn; ?></div> +<?php } ?> +<div id="response_options"> + <ul class="tabs"></ul> + <form id="task_note" + action="#tasks/<?php echo $task->getId(); ?>" + name="task_note" + method="post" enctype="multipart/form-data"> + <?php csrf_token(); ?> + <input type="hidden" name="id" value="<?php echo $task->getId(); ?>"> + <input type="hidden" name="a" value="postnote"> + <table width="100%" border="0" cellspacing="0" cellpadding="3"> + <tr> + <td> + <div> + <div class="faded" style="padding-left:0.15em"><?php + echo __('Note title - summary of the note (optional)'); ?></div> + <input type="text" name="title" id="title" size="60" value="<?php echo $info['title']; ?>" > + <br/> + <span class="error"> <?php echo $errors['title']; ?></span> + </div> + <div> + <label><strong><?php echo __('Internal Note'); ?></strong><span class='error'> * <?php echo $errors['note']; ?></span></label> + </div> + <textarea name="note" id="internal_note" cols="80" + placeholder="<?php echo __('Note details'); ?>" + rows="9" wrap="soft" data-draft-namespace="task.note" + data-draft-object-id="<?php echo $task->getId(); ?>" + class="richtext ifhtml draft draft-delete"><?php + echo $info['note']; + ?></textarea> + <div class="attachments"> + <?php + if ($task_note_form) + print $task_note_form->getField('attachments')->render(); + ?> + </div> + </td> + </tr> + <tr> + <td> + <div><?php echo __('Task Status');?> + <span class="faded"> - </span> + <select name="task_status"> + <option value="1" <?php + echo $task->isOpen() ? + 'selected="selected"': ''; ?>> <?php + echo _('Open'); ?></option> + <option value="0" <?php + echo $task->isClosed() ? + 'selected="selected"': ''; ?>> <?php + echo _('Closed'); ?></option> + </select> + <span class='error'><?php echo + $errors['task_status']; ?></span> + </div> + </td> + </tr> + </table> + <p style="padding-left:165px;"> + <input class="btn_sm" type="submit" value="<?php echo __('Post Note');?>"> + <input class="btn_sm" type="reset" value="<?php echo __('Reset');?>"> + </p> + </form> + </div> +<script type="text/javascript"> +$(function() { + $(document).on('click', 'li.active a#ticket_tasks', function(e) { + e.preventDefault(); + $('div#task_content').hide().empty(); + $('div#tasks_content').show(); + return false; + }); + $(document).off('.tf'); + $(document).on('submit.tf', 'form#task_note', function(e) { + e.preventDefault(); + var $form = $(this); + var $container = $('div#task_content'); + $.ajax({ + type: $form.attr('method'), + url: 'ajax.php/'+$form.attr('action').substr(1), + data: $form.serialize(), + cache: false, + success: function(resp, status, xhr) { + $container.html(resp); + $('#msg_notice, #msg_error',$container) + .delay(5000) + .slideUp(); + } + }) + .done(function() { }) + .fail(function() { }); + }); +}); +</script> diff --git a/include/staff/templates/task.tmpl.php b/include/staff/templates/task.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..bf1355bc0ac77b94c672f322eee7e1389674ccf4 --- /dev/null +++ b/include/staff/templates/task.tmpl.php @@ -0,0 +1,73 @@ +<?php + +if (!$info['title']) + $info['title'] = __('New Task'); + +?> +<div id="task-form"> +<h3><?php echo $info['title']; ?></h3> +<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b> +<hr/> +<?php + +if ($info['error']) { + echo sprintf('<p id="msg_error">%s</p>', $info['error']); +} elseif ($info['warning']) { + echo sprintf('<p id="msg_warning">%s</p>', $info['warning']); +} elseif ($info['msg']) { + echo sprintf('<p id="msg_notice">%s</p>', $info['msg']); +} ?> +<div id="new-task-form" style="display:block;"> +<form method="post" class="org" action="<?php echo $info['action'] ?: '#tasks/add'; ?>"> + <table width="100%" class="fixed"> + <?php + $form = $form ?: TaskForm::getInstance(); + $form->render(true, + __('Create New Task'), + array( + 'draft-namespace' => sprintf('ticket.%d.task', + $ticket->getId())) + ); + ?> + <tr><th colspan=2><em><?php + echo __('Task Visibility & Assignment'); ?></em></th></tr> + <?php + $iform = $iform ?: TaskForm::getInternalForm(); + foreach ($iform->getFields() as $name=>$field) { ?> + <tr> + <td class="multi-line <?php if ($field->get('required')) echo 'required'; + ?>" style="min-width:120px;" > + <?php echo Format::htmlchars($field->get('label')); ?>:</td> + <td> + <fieldset id="field<?php echo $field->getWidget()->id; + ?>" <?php if (!$field->isVisible()) echo 'style="display:none;"'; ?>> + <?php echo $field->render(); ?> + <?php if ($field->get('required')) { ?> + <span class="error">*</span> + <?php + } + foreach ($field->errors() as $E) { + ?><div class="error"><?php echo $E; ?></div><?php + } ?> + </fieldset> + </td> + </tr> + <?php + } + ?> + </table> + <hr> + <p class="full-width"> + <span class="buttons pull-left"> + <input type="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 __('Create Task'); ?>"> + </span> + </p> +</form> +</div> +<div class="clear"></div> +</div> diff --git a/include/staff/templates/ticket-preview.tmpl.php b/include/staff/templates/ticket-preview.tmpl.php index 55276c6d62b9d01aad30773a532d672295eb805a..05b78747f9b9f76ec699c965f3eaad2863689beb 100644 --- a/include/staff/templates/ticket-preview.tmpl.php +++ b/include/staff/templates/ticket-preview.tmpl.php @@ -29,7 +29,7 @@ elseif($msg) elseif($warn) echo sprintf('<div id="msg_warning">%s</div>',$warn); -echo '<ul class="tabs">'; +echo '<ul class="tabs" id="ticket-preview">'; echo ' <li class="active"><a id="preview_tab" href="#preview" @@ -42,7 +42,7 @@ echo sprintf(' $ticket->getNumCollaborators()); } echo '</ul>'; - +echo '<div id="ticket-preview_container">'; echo '<div class="tab_content" id="preview">'; echo '<table border="0" cellspacing="" cellpadding="1" width="100%" class="ticket_info">'; @@ -117,7 +117,7 @@ echo ' </table>'; echo '</div>'; // ticket preview content. ?> -<div class="tab_content" id="collab" style="display:none;"> +<div class="hidden tab_content" id="collab"> <table border="0" cellspacing="" cellpadding="1"> <colgroup><col style="min-width: 250px;"></col></colgroup> <?php @@ -146,6 +146,7 @@ echo '</div>'; // ticket preview content. ); ?> </div> +</div> <?php $options = array(); $options[]=array('action'=>sprintf(__('Thread (%d)'),$ticket->getThreadCount()),'url'=>"tickets.php?id=$tid"); diff --git a/include/staff/templates/tickets.tmpl.php b/include/staff/templates/tickets.tmpl.php index 31872def97e03e8c9b94616ce57587d964bf4a6e..6e6b6cf5c383bfceaca595fa135c478b887bb862 100644 --- a/include/staff/templates/tickets.tmpl.php +++ b/include/staff/templates/tickets.tmpl.php @@ -14,7 +14,7 @@ $from =' 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 ' .' LEFT JOIN '.USER_ACCOUNT_TABLE.' account ON (ticket.user_id=account.user_id) ' - .' LEFT JOIN '.DEPT_TABLE.' dept ON ticket.dept_id=dept.dept_id ' + .' LEFT JOIN '.DEPT_TABLE.' dept ON ticket.dept_id=dept.id ' .' LEFT JOIN '.STAFF_TABLE.' staff ON (ticket.staff_id=staff.staff_id) ' .' LEFT JOIN '.TEAM_TABLE.' team ON (ticket.team_id=team.team_id) ' .' LEFT JOIN '.TOPIC_TABLE.' topic ON (ticket.topic_id=topic.topic_id) ' @@ -40,12 +40,15 @@ while ($row = db_fetch_array($res)) if ($results) { $counts_sql = 'SELECT ticket.ticket_id, - count(DISTINCT attach.attach_id) as attachments, - count(DISTINCT thread.id) as thread_count, + count(DISTINCT attach.id) as attachments, + count(DISTINCT entry.id) as thread_count, count(DISTINCT collab.id) as collaborators - FROM '.TICKET_TABLE.' ticket - LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (ticket.ticket_id=attach.ticket_id) ' - .' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON ( ticket.ticket_id=thread.ticket_id) ' + FROM '.TICKET_TABLE.' ticket ' + .' LEFT JOIN '.THREAD_TABLE.' thread + ON (thread.object_id=ticket.ticket_id AND thread.object_type="T") ' + .' LEFT JOIN '.THREAD_ENTRY_TABLE.' entry ON (entry.thread_id=thread.id) ' + .' LEFT JOIN '.ATTACHMENT_TABLE.' attach + ON (attach.object_id=entry.id AND attach.`type` = "H") ' .' LEFT JOIN '.TICKET_COLLABORATOR_TABLE.' collab ON ( ticket.ticket_id=collab.ticket_id) ' .' WHERE ticket.ticket_id IN ('.implode(',', db_input(array_keys($results))).') @@ -137,9 +140,11 @@ if ($results) { ?> <?php } ?> <td align="center" nowrap> - <a class="Icon <?php echo strtolower($row['source']); ?>Ticket ticketPreview" + <a class="Icon <?php + echo strtolower($row['source']); ?>Ticket preview" title="<?php echo __('Preview Ticket'); ?>" - href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $tid; ?></a></td> + href="tickets.php?id=<?php echo $row['ticket_id']; ?>" + data-preview="#tickets/<?php echo $row['ticket_id']; ?>/preview"><?php echo $tid; ?></a></td> <td align="center" nowrap><?php echo Format::datetime($row['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 } ?> diff --git a/include/staff/templates/user-account.tmpl.php b/include/staff/templates/user-account.tmpl.php index 0bb5d0793a9a6b108c16b51a5cb7b06e9f821385..82674810bc82844d2a0afec587dfbfdeca6b1b1c 100644 --- a/include/staff/templates/user-account.tmpl.php +++ b/include/staff/templates/user-account.tmpl.php @@ -15,8 +15,7 @@ if ($info['error']) { } elseif ($info['msg']) { echo sprintf('<p id="msg_notice">%s</p>', $info['msg']); } ?> -<form method="post" class="user" action="#users/<?php echo $user->getId(); ?>/manage" > -<ul class="tabs"> +<ul class="tabs" id="user-account-tabs"> <li <?php echo !$access? 'class="active"' : ''; ?>><a href="#user-account" ><i class="icon-user"></i> <?php echo __('User Information'); ?></a></li> <li <?php echo $access? 'class="active"' : ''; ?>><a href="#user-access" @@ -25,6 +24,7 @@ if ($info['error']) { <input type="hidden" name="id" value="<?php echo $user->getId(); ?>" /> +<div id="user-account-tabs_container"> <div class="tab_content" id="user-account" style="display:<?php echo $access? 'none' : 'block'; ?>; margin:5px;"> <form method="post" class="user" action="#users/<?php echo $user->getId(); ?>/manage" > <input type="hidden" name="id" value="<?php echo $user->getId(); ?>" /> @@ -150,6 +150,7 @@ if ($info['error']) { </tbody> </table> </div> + </div> <hr> <p class="full-width"> <span class="buttons pull-left"> diff --git a/include/staff/templates/user-import.tmpl.php b/include/staff/templates/user-import.tmpl.php index ad57afc165b4c4ac140cd5d4031d4493c12f7bb4..b9ca02007d39bb21c5806edd74c79acbdd8216b8 100644 --- a/include/staff/templates/user-import.tmpl.php +++ b/include/staff/templates/user-import.tmpl.php @@ -10,6 +10,12 @@ if ($info['error']) { } elseif ($info['msg']) { echo sprintf('<p id="msg_notice">%s</p>', $info['msg']); } ?> +<ul class="tabs" id="user-import-tabs"> + <li class="active"><a href="#copy-paste" + ><i class="icon-edit"></i> <?php echo __('Copy Paste'); ?></a></li> + <li><a href="#upload" + ><i class="icon-fixed-width icon-cloud-upload"></i> <?php echo __('Upload'); ?></a></li> +</ul> <form action="<?php echo $info['action']; ?>" method="post" enctype="multipart/form-data" onsubmit="javascript: if ($(this).find('[name=import]').val()) { @@ -26,7 +32,7 @@ if ($info['error']) { if ($org_id) { ?> <input type="hidden" name="id" value="<?php echo $org_id; ?>"/> <?php } ?> - +<div id="user-import-tabs_container"> <div class="tab_content" id="copy-paste" style="margin:5px;"> <h2 style="margin-bottom:10px"><?php echo __('Name and Email'); ?></h2> <p><?php echo __( @@ -39,7 +45,7 @@ if ($org_id) { ?> </textarea> </div> -<div class="tab_content" id="upload" style="display:none;margin:5px;"> +<div class="hidden tab_content" id="upload" style="margin:5px;"> <h2 style="margin-bottom:10px"><?php echo __('Import a CSV File'); ?></h2> <p> <em><?php echo sprintf(__( @@ -71,6 +77,7 @@ if ($org_id) { ?> </tr></table> <br/> <input type="file" name="import"/> +</div> </div> <hr> <p class="full-width"> diff --git a/include/staff/templates/user.tmpl.php b/include/staff/templates/user.tmpl.php index aabd5cd17e3f488c3d014e201d08fa439099e8fd..4737aaea25e1d0c5b923912c7d9dc1a67c197622 100644 --- a/include/staff/templates/user.tmpl.php +++ b/include/staff/templates/user.tmpl.php @@ -34,11 +34,11 @@ if ($info['error']) { } ?> <div class="clear"></div> -<ul class="tabs" style="margin-top:5px"> +<ul class="tabs" id="user_tabs" style="margin-top:5px"> <li class="active"><a href="#info-tab" ><i class="icon-info-sign"></i> <?php echo __('User'); ?></a></li> <?php if ($org) { ?> - <li><a href="#organization-tab" + <li><a href="#org-tab" ><i class="icon-fixed-width icon-building"></i> <?php echo __('Organization'); ?></a></li> <?php } $ext_id = "U".$user->getId(); @@ -47,6 +47,7 @@ if ($info['error']) { ><i class="icon-fixed-width icon-pushpin"></i> <?php echo __('Notes'); ?></a></li> </ul> +<div id="user_tabs_container"> <div class="tab_content" id="info-tab"> <div class="floating-options"> <a href="<?php echo $info['useredit'] ?: '#'; ?>" id="edituser" class="action" title="<?php echo __('Edit'); ?>"><i class="icon-edit"></i></a> @@ -70,7 +71,7 @@ if ($info['error']) { </div> <?php if ($org) { ?> -<div class="tab_content" id="organization-tab" style="display:none"> +<div class="hidden tab_content" id="org-tab"> <div class="floating-options"> <a href="orgs.php?id=<?php echo $org->getId(); ?>" title="<?php echo __('Manage Organization'); ?>" class="action"><i class="icon-share"></i></a> @@ -92,7 +93,7 @@ if ($info['error']) { </div> <?php } # endif ($org) ?> -<div class="tab_content" id="notes-tab" style="display:none"> +<div class="hidden tab_content" id="notes-tab"> <?php $show_options = true; foreach ($notes as $note) include STAFFINC_DIR . 'templates/note.tmpl.php'; @@ -107,6 +108,7 @@ foreach ($notes as $note) </div> </div> </div> +</div> </div> <div id="user-form" style="display:<?php echo $forms ? 'block' : 'none'; ?>;"> diff --git a/include/staff/templates/users.tmpl.php b/include/staff/templates/users.tmpl.php index 47358580bf66bb48be0d9d9ec8f8b22385827b3e..51f8f7e3d6b84fa57c8174aae8a4e2547fa7ea9e 100644 --- a/include/staff/templates/users.tmpl.php +++ b/include/staff/templates/users.tmpl.php @@ -99,8 +99,10 @@ if ($num) { ?> value="<?php echo $row['id']; ?>" <?php echo $sel?'checked="checked"':''; ?> > </td> <td> - <a class="userPreview" - href="users.php?id=<?php echo $row['id']; ?>"><?php + <a class="preview" + href="users.php?id=<?php echo $row['id']; ?>" + data-preview="#users/<?php + echo $row['id']; ?>/preview" ><?php echo Format::htmlchars($name); ?></a> <?php diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php index 7ec099aff6b315bbc6180c6f2f88b5f8841e0b40..642808356b95b644a5a0b9a99921c601cd57f714 100644 --- a/include/staff/ticket-open.inc.php +++ b/include/staff/ticket-open.inc.php @@ -272,7 +272,7 @@ if ($_POST) <tbody> <?php //is the user allowed to post replies?? - if($thisstaff->getRole()->canPostReply()) { ?> + if($thisstaff->getRole()->canPostTicketReply()) { ?> <tr> <th colspan="2"> <em><strong><?php echo __('Response');?></strong>: <?php echo __('Optional response to the above issue.');?></em> diff --git a/include/staff/ticket-tasks.inc.php b/include/staff/ticket-tasks.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..d4cf59e5f133cd06dc5d243872752fa6e61c6429 --- /dev/null +++ b/include/staff/ticket-tasks.inc.php @@ -0,0 +1,163 @@ +<?php + +$tasks = Task::objects() + ->select_related('dept', 'staff') + ->order_by('-created'); + + +$count = $tasks->count(); +$pageNav = new Pagenate($count,1, 100000); //TODO: support ajax based pages +$showing = $pageNav->showing().' '._N('task', 'tasks', $count); + +?> +<div id="tasks_content" style="display:block;"> +<div style="width:700px; float:left;"> + <?php + if ($count) { + echo '<strong>'.$showing.'</strong>'; + } else { + echo sprintf(__('%s does not have any tasks'), $ticket? 'This ticket' : + 'System'); + } + ?> +</div> +<div style="float:right;text-align:right;padding-right:5px;"> + <?php + if ($ticket) { ?> + <a + class="Icon newTicket task-action" + data-url="tickets.php?id=<?php echo $ticket->getId(); ?>#tasks" + data-dialog='{"size":"large"}' + href="#tickets/<?php + echo $ticket->getId(); ?>/add-task"> <?php + print __('Add New Task'); ?></a> + <?php + } ?> +</div> +<br/> +<div> +<?php +if ($count) { ?> +<form action="#tickets/<?php echo $ticket->getId(); ?>/tasks" method="POST" name='tasks' style="padding-top:10px;"> +<?php csrf_token(); ?> + <input type="hidden" name="a" value="mass_process" > + <input type="hidden" name="do" id="action" value="" > + <table class="list" border="0" cellspacing="1" cellpadding="2" width="940"> + <thead> + <tr> + <?php + //TODO: support mass actions. + if (0) {?> + <th width="8px"> </th> + <?php + } ?> + <th width="70"><?php echo __('Number'); ?></th> + <th width="100"><?php echo __('Date'); ?></th> + <th width="100"><?php echo __('Status'); ?></th> + <th width="300"><?php echo __('Title'); ?></th> + <th width="200"><?php echo __('Department'); ?></th> + <th width="200"><?php echo __('Assignee'); ?></th> + </tr> + </thead> + <tbody class="tasks"> + <?php + foreach($tasks as $task) { + $id = $task->getId(); + $assigned=''; + if ($task->staff) + $assigned=sprintf('<span class="Icon staffAssigned">%s</span>', + Format::truncate($task->staff->getName(),40)); + + $status = $task->isOpen() ? '<strong>open</strong>': 'closed'; + + $title = Format::htmlchars(Format::truncate($task->getTitle(),40)); + $threadcount = $task->getThread() ? + $task->getThread()->getNumEntries() : 0; + ?> + <tr id="<?php echo $id; ?>"> + <?php + //Implement mass action....if need be. + if (0) { ?> + <td align="center" class="nohover"> + <input class="ckb" type="checkbox" name="tids[]" + value="<?php echo $id; ?>" <?php echo $sel?'checked="checked"':''; ?>> + </td> + <?php + } ?> + <td align="center" nowrap> + <a class="Icon no-pjax preview" + title="<?php echo __('Preview Task'); ?>" + href="#tasks/<?php echo $id; ?>/view" + data-preview="#tasks/<?php echo $id; ?>/preview" + ><?php echo $task->getNumber(); ?></a></td> + <td align="center" nowrap><?php echo + Format::datetime($task->created); ?></td> + <td><?php echo $status; ?></td> + <td><a <?php if ($flag) { ?> class="no-pjax" + title="<?php echo ucfirst($flag); ?> Task" <?php } ?> + href="#tasks/<?php echo $id; ?>/view"><?php + echo $title; ?></a> + <?php + if ($threadcount>1) + echo "<small>($threadcount)</small> ".'<i + class="icon-fixed-width icon-comments-alt"></i> '; + if ($row['collaborators']) + echo '<i class="icon-fixed-width icon-group faded"></i> '; + if ($row['attachments']) + echo '<i class="icon-fixed-width icon-paperclip"></i> '; + ?> + </td> + <td><?php echo Format::truncate($task->dept->getName(), 40); ?></td> + <td> <?php echo $assigned; ?></td> + </tr> + <?php + } + ?> + </tbody> +</table> +</form> +<?php + } ?> +</div> +</div> +<div id="task_content" style="display:none;"> +</div> +<script type="text/javascript"> +$(function() { + $(document).on('click.tasks', 'tbody.tasks a, a#reload-task', function(e) { + e.preventDefault(); + var url = 'ajax.php/'+$(this).attr('href').substr(1); + var $container = $('div#task_content'); + $container.load(url, function () { + $('.tip_box').remove(); + $('div#tasks_content').hide(); + }).show(); + return false; + }); + $(document).off('.task-action'); + $(document).on('click.task-action', 'a.task-action', function(e) { + e.preventDefault(); + var url = 'ajax.php/' + +$(this).attr('href').substr(1) + +'?_uid='+new Date().getTime(); + var $redirect = $(this).data('href'); + var $options = $(this).data('dialog'); + $.dialog(url, [201], function (xhr) { + var tid = parseInt(xhr.responseText); + if (tid) { + var url = 'ajax.php/tasks/'+tid+'/view'; + var $container = $('div#task_content'); + $container.load(url, function () { + $('.tip_box').remove(); + $('div#tasks_content').hide(); + }).show(); + } else { + window.location.href = $redirect ? $redirect : window.location.href; + } + }, $options); + return false; + }); + + +}); +</script> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index abdb74ed1a71040920afeb5f46cea4c3fc8da4d3..e6522e7dee47fbb44e6a87aeb10e35379828b5e3 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -376,8 +376,15 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) { $tcount = $ticket->getThreadCount(); $tcount+= $ticket->getNumNotes(); ?> -<ul id="threads"> - <li><a class="active" id="toggle_ticket_thread" href="#"><?php echo sprintf(__('Ticket Thread (%d)'), $tcount); ?></a></li> +<ul class="tabs threads" id="ticket_tabs" > + <li class="active"><a href="#ticket_thread"><?php echo sprintf(__('Ticket Thread (%d)'), $tcount); ?></a></li> + <li><a id="ticket_tasks" href="#tasks" + data-url="<?php + echo sprintf('#tickets/%d/tasks', $ticket->getId()); ?>"><?php + echo __('Tasks'); + if ($ticket->getNumTasks()) + echo sprintf(' (%d)', $ticket->getNumTasks()); + ?></a></li> </ul> <div id="ticket_tabs_container"> <div id="ticket_thread" class="tab_content"> @@ -387,7 +394,7 @@ $tcount+= $ticket->getNumNotes(); $types = array('M', 'R', 'N'); if(($thread=$ticket->getThreadEntries($types))) { foreach($thread as $entry) { ?> - <table class="thread-entry <?php echo $threadTypes[$entry['thread_type']]; ?>" cellspacing="0" cellpadding="1" width="940" border="0"> + <table class="thread-entry <?php echo $threadTypes[$entry['type']]; ?>" cellspacing="0" cellpadding="1" width="940" border="0"> <tr> <th colspan="4" width="100%"> <div> @@ -410,12 +417,13 @@ $tcount+= $ticket->getNumNotes(); echo $entry['id']; ?>"><div><?php echo $entry['body']->toHtml(); ?></div></td></tr> <?php + $urls = null; if($entry['attachments'] && ($tentry = $ticket->getThreadEntry($entry['id'])) && ($urls = $tentry->getAttachmentUrls()) && ($links = $tentry->getAttachmentsLinks())) {?> <tr> - <td class="info" colspan="4"><?php echo $tentry->getAttachmentsLinks(); ?></td> + <td class="info" colspan="4"><?php echo $links; ?></td> </tr> <?php } if ($urls) { ?> @@ -429,13 +437,12 @@ $tcount+= $ticket->getNumNotes(); } ?> </table> <?php - if($entry['thread_type']=='M') - $msgId=$entry['id']; + if ($entry['type'] == 'M') + $msgId = $entry['id']; } } else { echo '<p>'.__('Error fetching ticket thread - get technical help.').'</p>'; }?> -</div> <div class="clear" style="padding-bottom:10px;"></div> <?php if($errors['err']) { ?> <div id="msg_error"><?php echo $errors['err']; ?></div> @@ -448,11 +455,11 @@ $tcount+= $ticket->getNumNotes(); <div id="response_options"> <ul class="tabs"> <?php - if ($role->canPostReply()) { ?> + if ($role->canPostTicketReply()) { ?> <li class="active"><a href="#reply"><?php echo __('Post Reply');?></a></li> <?php } ?> - <li><a id="note_tab" href="#note"><?php echo __('Post Internal Note');?></a></li> + <li><a href="#note"><?php echo __('Post Internal Note');?></a></li> <?php if ($role->canTransferTickets()) { ?> <li><a href="#transfer"><?php echo __('Department Transfer');?></a></li> @@ -466,7 +473,7 @@ $tcount+= $ticket->getNumNotes(); } ?> </ul> <?php - if ($role->canPostReply()) { ?> + if ($role->canPostTicketReply()) { ?> <form id="reply" class="tab_content" action="tickets.php?id=<?php echo $ticket->getId(); ?>" name="reply" method="post" enctype="multipart/form-data"> <?php csrf_token(); ?> @@ -578,9 +585,9 @@ $tcount+= $ticket->getNumNotes(); echo $attrs; ?>><?php echo $draft ?: $info['response']; ?></textarea> <div id="reply_form_attachments" class="attachments"> -<?php -print $response_form->getField('attachments')->render(); -?> + <?php + print $response_form->getField('attachments')->render(); + ?> </div> </td> </tr> @@ -654,7 +661,8 @@ print $response_form->getField('attachments')->render(); </form> <?php } ?> - <form id="note" class="tab_content" action="tickets.php?id=<?php echo $ticket->getId(); ?>#note" name="note" method="post" enctype="multipart/form-data"> + <form id="note" class="hidden tab_content" action="tickets.php?id=<?php + echo $ticket->getId(); ?>#note" name="note" method="post" enctype="multipart/form-data"> <?php csrf_token(); ?> <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>"> <input type="hidden" name="locktime" value="<?php echo $cfg->getLockTime(); ?>"> @@ -691,9 +699,9 @@ print $response_form->getField('attachments')->render(); echo $attrs; ?>><?php echo $draft ?: $info['note']; ?></textarea> <div class="attachments"> -<?php -print $note_form->getField('attachments')->render(); -?> + <?php + print $note_form->getField('attachments')->render(); + ?> </div> </td> </tr> @@ -892,6 +900,8 @@ print $note_form->getField('attachments')->render(); </form> <?php } ?> + </div> + </div> </div> <div style="display:none;" class="dialog" id="print-options"> <h3><?php echo __('Ticket Print Options');?></h3> diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index 4507c7bea5251831a134cc50b85dbd0207ffe8b2..076f0e9adc34a54d30060e78947b54b4623ae338 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -150,6 +150,7 @@ TicketForm::ensureDynamicDataView(); // Save the query to the session for exporting $_SESSION[':Q:tickets'] = $tickets; + ?> <!-- SEARCH FORM START --> @@ -303,8 +304,11 @@ $_SESSION[':Q:tickets'] = $tickets; </td> <?php } ?> <td title="<?php echo $T['user__default_email__address']; ?>" nowrap> - <a class="Icon <?php echo strtolower($T['source']); ?>Ticket ticketPreview" title="Preview Ticket" - href="tickets.php?id=<?php echo $T['ticket_id']; ?>"><?php echo $tid; ?></a></td> + <a class="Icon <?php echo strtolower($T['source']); ?>Ticket preview" + title="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($T[$date_col ?: 'lastupdate']); ?></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']; ?>"><?php echo $subject; ?></a> diff --git a/include/staff/user-view.inc.php b/include/staff/user-view.inc.php index edc9464c97907a108e1a5bc2f7803b2f33e392ad..6d29d8b2492287165369760ba3ca7320eaaa9526 100644 --- a/include/staff/user-view.inc.php +++ b/include/staff/user-view.inc.php @@ -124,27 +124,28 @@ $org = $user->getOrganization(); </table> <br> <div class="clear"></div> -<ul class="tabs"> - <li class="active"><a id="tickets_tab" href="#tickets"><i +<ul class="tabs" id="user-view-tabs"> + <li class="active"><a href="#tickets"><i class="icon-list-alt"></i> <?php echo __('User Tickets'); ?></a></li> - <li><a id="notes_tab" href="#notes"><i + <li><a href="#notes"><i class="icon-pushpin"></i> <?php echo __('Notes'); ?></a></li> </ul> -<div id="tickets" class="tab_content"> -<?php -include STAFFINC_DIR . 'templates/tickets.tmpl.php'; -?> -</div> +<div id="user-view-tabs_container"> + <div id="tickets" class="tab_content"> + <?php + include STAFFINC_DIR . 'templates/tickets.tmpl.php'; + ?> + </div> -<div class="tab_content" id="notes" style="display:none"> -<?php -$notes = QuickNote::forUser($user); -$create_note_url = 'users/'.$user->getId().'/note'; -include STAFFINC_DIR . 'templates/notes.tmpl.php'; -?> + <div class="hidden tab_content" id="notes"> + <?php + $notes = QuickNote::forUser($user); + $create_note_url = 'users/'.$user->getId().'/note'; + include STAFFINC_DIR . 'templates/notes.tmpl.php'; + ?> + </div> </div> - -<div style="display:none;" class="dialog" id="confirm-action"> +<div class="hidden dialog" id="confirm-action"> <h3><?php echo __('Please Confirm'); ?></h3> <a class="close" href=""><i class="icon-remove-circle"></i></a> <hr/> diff --git a/include/staff/users.inc.php b/include/staff/users.inc.php index 3c1152e918d6f87d58a031131b137459b218b8d3..073c33a03431a9c39fd3007c95649d3733e77e92 100644 --- a/include/staff/users.inc.php +++ b/include/staff/users.inc.php @@ -124,7 +124,9 @@ else ?> <tr id="<?php echo $U['id']; ?>"> <td> - <a class="userPreview" href="users.php?id=<?php echo $U['id']; ?>"><?php + <a class="preview" + href="users.php?id=<?php echo $U['id']; ?>" + data-preview="#users/<?php echo $U['id']; ?>/preview"><?php echo Format::htmlchars($name); ?></a> <?php diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig index ca8a3dae54ce2f95a30f3729ff58cffbde89ef0b..2888135a10a7347cf981c5bc3bd82404849bde5d 100644 --- a/include/upgrader/streams/core.sig +++ b/include/upgrader/streams/core.sig @@ -1 +1 @@ -36f6b32893c2b97c5104ab5302d2dd2e +e7a338f95ed622678123386a8a59dfc0 diff --git a/include/upgrader/streams/core/36f6b328-e7a338f9.cleanup.sql b/include/upgrader/streams/core/36f6b328-e7a338f9.cleanup.sql new file mode 100644 index 0000000000000000000000000000000000000000..800c8127673c19f3628eb3b4b385a691cac6e96b --- /dev/null +++ b/include/upgrader/streams/core/36f6b328-e7a338f9.cleanup.sql @@ -0,0 +1,11 @@ +ALTER TABLE `%TABLE_PREFIX%thread` + DROP COLUMN `tid`; + +ALTER TABLE `%TABLE_PREFIX%thread_entry` + DROP COLUMN `ticket_id`; + +DROP TABLE `%TABLE_PREFIX%ticket_attachment`; + +OPTIMIZE TABLE `%TABLE_PREFIX%ticket`; +OPTIMIZE TABLE `%TABLE_PREFIX%thread`; +OPTIMIZE TABLE `%TABLE_PREFIX%thread_entry`; diff --git a/include/upgrader/streams/core/36f6b328-e7a338f9.patch.sql b/include/upgrader/streams/core/36f6b328-e7a338f9.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..0150b4fd1b4ad4e7a7106e56c32f0140406e6ca4 --- /dev/null +++ b/include/upgrader/streams/core/36f6b328-e7a338f9.patch.sql @@ -0,0 +1,123 @@ +/** + * @version v1.9.6 + * @signature e7a338f95ed622678123386a8a59dfc0 + * @title Add support for ticket tasks + * + * This patch adds ability to thread anything and introduces tasks + * + */ + +-- Add thread table +DROP TABLE IF EXISTS `%TABLE_PREFIX%thread`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%thread` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `object_id` int(11) unsigned NOT NULL, + `object_type` char(1) NOT NULL, + `extra` text, + `created` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `object_id` (`object_id`), + KEY `object_type` (`object_type`) +) DEFAULT CHARSET=utf8; + +-- create threads +INSERT INTO `%TABLE_PREFIX%thread` + (`object_id`, `object_type`, `created`) + SELECT t1.ticket_id, 'T', t1.created + FROM `%TABLE_PREFIX%ticket_thread` t1 + JOIN ( + SELECT ticket_id, MIN(id) as id + FROM `%TABLE_PREFIX%ticket_thread` + WHERE `thread_type` = 'M' + GROUP BY ticket_id + ) t2 + ON (t1.ticket_id=t2.ticket_id and t1.id=t2.id) + ORDER BY t1.created; + +ALTER TABLE `%TABLE_PREFIX%ticket_thread` + ADD `thread_id` INT( 11 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `pid` , + ADD INDEX ( `thread_id` ); + +UPDATE `%TABLE_PREFIX%ticket_thread` t1 + LEFT JOIN `%TABLE_PREFIX%thread` t2 ON ( t2.object_id = t1.ticket_id ) + SET t1.thread_id = t2.id; + +-- convert ticket_thread to thread_entry +ALTER TABLE `%TABLE_PREFIX%ticket_thread` + CHANGE `thread_type` `type` CHAR( 1 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + ADD INDEX ( `type` ); + +RENAME TABLE `%TABLE_PREFIX%ticket_thread` TO `%TABLE_PREFIX%thread_entry` ; + +-- add thread id to ticket table +ALTER TABLE `%TABLE_PREFIX%ticket` + ADD `thread_id` INT( 11 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `number` , + ADD INDEX ( `thread_id` ); + +UPDATE `%TABLE_PREFIX%ticket` t1 + LEFT JOIN `%TABLE_PREFIX%thread` t2 ON ( t2.object_id = t1.ticket_id ) + SET t1.thread_id = t2.id; + +-- move records in ticket_attachment to generic attachment table +ALTER TABLE `%TABLE_PREFIX%attachment` + DROP PRIMARY KEY, + ADD `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST, + ADD UNIQUE `file-type` (`object_id`, `file_id`, `type`); + +INSERT INTO `%TABLE_PREFIX%attachment` + (`object_id`, `type`, `file_id`, `inline`) + SELECT `ref_id`, 'H', `file_id`, `inline` + FROM `%TABLE_PREFIX%ticket_attachment`; + +-- convert ticket_email_info to thread_entry_email +ALTER TABLE `%TABLE_PREFIX%ticket_email_info` + CHANGE `thread_id` `thread_entry_id` INT( 11 ) UNSIGNED NOT NULL, + CHANGE `email_mid` `mid` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + ADD INDEX ( `thread_entry_id` ); + +RENAME TABLE `%TABLE_PREFIX%ticket_email_info` TO `%TABLE_PREFIX%thread_entry_email`; + +-- create task task +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%task` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `object_id` int(11) NOT NULL DEFAULT '0', + `object_type` char(1) NOT NULL, + `number` varchar(20) DEFAULT NULL, + `dept_id` int(10) unsigned NOT NULL DEFAULT '0', + `sla_id` int(10) unsigned NOT NULL DEFAULT '0', + `staff_id` int(10) unsigned NOT NULL DEFAULT '0', + `team_id` int(10) unsigned NOT NULL DEFAULT '0', + `flags` int(10) unsigned NOT NULL DEFAULT '0', + `duedate` datetime DEFAULT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `dept_id` (`dept_id`), + KEY `staff_id` (`staff_id`), + KEY `team_id` (`team_id`), + KEY `created` (`created`), + KEY `sla_id` (`sla_id`), + KEY `object` (`object_id`,`object_type`) +) DEFAULT CHARSET=utf8; + +-- rename ticket sequence numbering + +UPDATE `%TABLE_PREFIX%config` + SET `key` = 'ticket_number_format' + WHERE `key` = 'number_format' AND `namespace` = 'core'; + +UPDATE `%TABLE_PREFIX%config` + SET `key` = 'ticket_sequence_id' + WHERE `key` = 'sequence_id' AND `namespace` = 'core'; + +ALTER TABLE `%TABLE_PREFIX%department` + ADD `pid` int(11) unsigned default NULL AFTER `id`, + ADD `path` varchar(128) NOT NULL default '/' AFTER `message_auto_response`; + +UPDATE `%TABLE_PREFIX%department` + SET `path` = CONCAT('/', id, '/'); + +-- Set new schema signature +UPDATE `%TABLE_PREFIX%config` + SET `value` = 'e7a338f95ed622678123386a8a59dfc0' + WHERE `key` = 'schema_signature' AND `namespace` = 'core'; diff --git a/include/upgrader/streams/core/36f6b328-e7a338f9.task.php b/include/upgrader/streams/core/36f6b328-e7a338f9.task.php new file mode 100644 index 0000000000000000000000000000000000000000..eec7c43f72b3fe48f18ad2c0feb64824cb732f17 --- /dev/null +++ b/include/upgrader/streams/core/36f6b328-e7a338f9.task.php @@ -0,0 +1,58 @@ +<?php +/* + * Import initial form for task + * + */ + +class TaskLoader extends MigrationTask { + var $description = "Loading initial data for tasks"; + static $pman = array( + 'ticket.create' => 'task.create', + 'ticket.edit' => 'task.edit', + 'ticket.reply' => 'task.reply', + 'ticket.delete' => 'task.delete', + 'ticket.close' => 'task.close', + 'ticket.assign' => 'task.assign', + 'ticket.transfer' => 'task.transfer', + ); + + function run($max_time) { + global $cfg; + + // Load task form + require_once INCLUDE_DIR.'class.task.php'; + Task::__loadDefaultForm(); + // Load sequence for the task + $i18n = new Internationalization($cfg->get('system_language', 'en_US')); + $sequences = $i18n->getTemplate('sequence.yaml')->getData(); + foreach ($sequences as $s) { + if ($s['id'] != 2) continue; + unset($s['id']); + $sq=Sequence::create($s); + $sq->save(); + $sql= 'INSERT INTO '.CONFIG_TABLE + .' (`namespace`, `key`, `value`) ' + .' VALUES + ("core", "task_number_format", "###"), + ("core", "task_sequence_id",'.db_input($sq->id).')'; + db_query($sql); + break; + } + + // Copy ticket permissions + foreach (Role::objects() as $role) { + $perms = $role->getPermissionInfo(); + foreach (self::$pmap as $k => $v) { + if (in_array($k, $perms)) + $perms[] = $v; + } + $role->updatePerms($perms); + $role->save(); + } + + } +} + +return 'TaskLoader'; + +?> diff --git a/scp/ajax.php b/scp/ajax.php index dc0d35e34821a50a57ff886ff52377c806781846..1c313c18c4afab80c58cbb3579de2cf254a34c76 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -144,6 +144,8 @@ $dispatcher = patterns('', url_post('^(?P<tid>\d+)/status$', 'setTicketStatus'), url_get('^status/(?P<status>\w+)(?:/(?P<sid>\d+))?$', 'changeSelectedTicketsStatus'), url_post('^status/(?P<state>\w+)$', 'setSelectedTicketsStatus'), + url_get('^(?P<tid>\d+)/tasks$', 'tasks'), + url('^(?P<tid>\d+)/add-task$', 'addTask'), url_get('^lookup', 'lookup'), url('^search', patterns('ajax.search.php:SearchAjaxAPI', url_get('^$', 'getAdvancedSearchDialog'), @@ -156,6 +158,19 @@ $dispatcher = patterns('', url_get('^/field/(?P<id>[\w_!:]+)$', 'addField') )) )), + url('^/tasks/', patterns('ajax.tasks.php:TasksAjaxAPI', + url_get('^(?P<tid>\d+)/preview$', 'preview'), + url_get('^(?P<tid>\d+)/edit', 'edit'), + url_post('^(?P<tid>\d+)/edit$', 'edit'), + url_get('^(?P<tid>\d+)/transfer', 'transfer'), + url_post('^(?P<tid>\d+)/transfer$', 'transfer'), + url_get('^(?P<tid>\d+)/assign', 'assign'), + url_post('^(?P<tid>\d+)/assign$', 'assign'), + url_get('^(?P<tid>\d+)/delete', 'delete'), + url_post('^(?P<tid>\d+)/delete$', 'delete'), + url_get('^(?P<tid>\d+)/view$', 'task'), + url_post('^(?P<tid>\d+)$', 'task') + )), url('^/collaborators/', patterns('ajax.tickets.php:TicketsAjaxAPI', url_get('^(?P<cid>\d+)/view$', 'viewCollaborator'), url_post('^(?P<cid>\d+)$', 'updateCollaborator') diff --git a/scp/attachment.php b/scp/attachment.php index 07f20981a19a982c3014033eab67465b9f5587a8..ab6d45ab14d31d4122c085f40188c125bb74d897 100644 --- a/scp/attachment.php +++ b/scp/attachment.php @@ -16,15 +16,22 @@ require('staff.inc.php'); require_once(INCLUDE_DIR.'class.attachment.php'); -//Basic checks -if(!$thisstaff || !$_GET['id'] || !$_GET['h'] +// Basic checks +if (!$thisstaff + || !$_GET['id'] + || !$_GET['h'] || !($attachment=Attachment::lookup($_GET['id'])) - || !($file=$attachment->getFile())) + || !($file=$attachment->getFile()) + || strcasecmp(trim($_GET['h']), $file->getDownloadHash()) + || !($object=$attachment->getObject()) + || !$object instanceof ThreadEntry + || !($ticket=$object->getThread()->getObject()) + || !$ticket instanceof Ticket + ) Http::response(404, __('Unknown or invalid file')); -//Validate session access hash - we want to make sure the link is FRESH! and the user has access to the parent ticket!! -$vhash=md5($attachment->getFileId().session_id().strtolower($file->getKey())); -if(strcasecmp(trim($_GET['h']),$vhash) || !($ticket=$attachment->getTicket()) || !$ticket->checkStaffAccess($thisstaff)) die(__('Access Denied')); +if (!$ticket->checkStaffAccess($thisstaff)) + die(__('Access Denied')); //Download the file.. $file->download(); diff --git a/scp/css/scp.css b/scp/css/scp.css index b3a05e7a8cf7b60d80014a880d37d86dc257ca6c..0d76bf2ba467afd9ff21f10e99c9f039bdc3fb2e 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -459,7 +459,7 @@ table.list thead th { color:#000; text-align:left; vertical-align:top; - padding: 2px 4px; + padding: 4px 5px; } table.list th a { @@ -468,7 +468,7 @@ table.list th a { color:#000; } -table.list thead th a { padding: 3px; padding-right: 15px; display: block; white-space: nowrap; color: #000; background: url('../images/asc_desc.gif') 100% 50% no-repeat; } +table.list thead th a { padding-right: 15px; display: block; white-space: nowrap; color: #000; background: url('../images/asc_desc.gif') 100% 50% no-repeat; } table.list thead th a.asc { background: url('../images/asc.gif') 100% 50% no-repeat #cfe6ff; } table.list thead th a.desc { background: url('../images/desc.gif') 100% 50% no-repeat #cfe6ff; } @@ -774,40 +774,6 @@ h2 .reload { margin:0; } -#threads { - margin:0; - padding:5px 10px 0 10px; - border:1px solid #aaa; - background:#F4FAFF; - height:30px; -} - -#threads li { - list-style:none; - margin:0; - padding:0; - display:inline; -} - -#threads li a { - display:inline-block; - width:auto; - height:30px; - line-height:30px; - border-top:1px solid #F4FAFF; - padding:0 10px 0 32px; - margin-right:10px; -} - -#threads li a.active { - height:29px; - background-color:#fff; - border:1px solid #aaa; - border-bottom:none; - border-top:2px solid #ed9100; - font-weight:bold; -} - #toggle_ticket_thread { background:url(../images/icons/open.gif) 10px 50% no-repeat; } @@ -816,9 +782,7 @@ h2 .reload { background:url(../images/icons/note.gif) 10px 50% no-repeat; } -#ticket_thread table.message, -#ticket_thread table.response, -#ticket_thread table.note { +table.thread-entry { margin-top:10px; border:1px solid #aaa; border-bottom:2px solid #aaa; @@ -830,7 +794,7 @@ h2 .reload { border-bottom:2px solid #ddd; } -#ticket_thread table th, #ticket_notes table th { +table.thread-entry th, #ticket_notes table th { text-align:left; border-bottom:1px solid #aaa; font-size:10pt; @@ -858,15 +822,15 @@ h2 .reload { text-align:right; } -#ticket_thread > .message th { +.thread-entry.message th { background:#C3D9FF; } -#ticket_thread > .response th { +.thread-entry.response th { background:#FFE0B3; } -#ticket_thread > .note th { +.thread-entry.note th { background:#FFE; } @@ -878,7 +842,7 @@ h2 .reload { background:#f9f9f9; } -#ticket_thread .info, #ticket_notes .info { +.thread-entry .info, #ticket_notes .info { padding:5px; background:#F4FAFF; height:16px; @@ -1413,6 +1377,18 @@ time { overflow-y: auto; } +.dialog#popup { + width:650px; +} + +.dialog.size-normal { + width:650px !important; +} + +.dialog.size-large { + width:750px !important; +} + .dialog #popup-loading { position:absolute; text-align:center; diff --git a/scp/js/scp.js b/scp/js/scp.js index 5c3101c667d134e0dfb1957ace6105d1f2bef1a6..1dfeefd1d48d7c76b5cefe85c04b8de7db1fe13e 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -321,7 +321,7 @@ var scp_prep = function() { }); //Dialog - $('.dialog').each(function() { + $('.dialog').resize(function() { var w = $(window), $this=$(this); $this.css({ top : (w.innerHeight() / 7), @@ -330,9 +330,18 @@ var scp_prep = function() { $this.hasClass('draggable') && $this.draggable({handle:'h3'}); }); + + $('.dialog').each(function() { + $this=$(this); + $this.resize(); + $this.hasClass('draggable') && $this.draggable({handle:'h3'}); + }); + $('.dialog').delegate('input.close, a.close', 'click', function(e) { e.preventDefault(); - $(this).parents('div.dialog').hide() + $(this).parents('div.dialog') + .hide() + .removeAttr('style'); $.toggleOverlay(false); return false; @@ -542,11 +551,18 @@ $.dialog = function (url, codes, cb, options) { var $popup = $('.dialog#popup'); + $popup.attr('class', + function(pos, classes) { + return classes.replace(/\bsize-\S+/g, ''); + }); + + $popup.addClass(options.size ? ('size-'+options.size) : 'size-normal'); + $.toggleOverlay(true); $('div.body', $popup).empty().hide(); $('div#popup-loading', $popup).show() .find('h1').css({'margin-top':function() { return $popup.height()/3-$(this).height()/3}}); - $popup.show(); + $popup.resize().show(); $('div.body', $popup).load(url, function () { $('div#popup-loading', $popup).hide(); $('div.body', $popup).slideDown({ @@ -626,7 +642,7 @@ $.orgLookup = function (url, cb) { $.uid = 1; -//Tabs +// Tabs $(document).on('click.tab', 'ul.tabs li a', function(e) { e.preventDefault(); var $this = $(this), @@ -661,6 +677,7 @@ $(document).on('click.tab', 'ul.tabs li a', function(e) { $tab.fadeIn('fast'); return false; } + }); $.changeHash = function(hash, quiet) { if (quiet) { @@ -731,7 +748,7 @@ $(document).on('pjax:click', function(options) { $(document).stop(false, true); // Remove tips and clear any pending timer - $('.tip, .help-tips, .userPreview, .ticketPreview, .previewfaq').each(function() { + $('.tip, .help-tips, .previewfaq, .preview').each(function() { if ($(this).data('timer')) clearTimeout($(this).data('timer')); }); diff --git a/scp/js/ticket.js b/scp/js/ticket.js index 8a1bbc6ecae18dd2001b22b6b682461a367c6227..600d8186bf7b0e63824daf67b9fdbe174807aacf 100644 --- a/scp/js/ticket.js +++ b/scp/js/ticket.js @@ -319,44 +319,6 @@ $.refreshTicketView = function() { } var ticket_onload = function($) { - if (!location.hash || !$('#response_options .tab_content' + location.hash).length) { - $('#response_options ul.tabs li:first a').trigger('click'); - } - - $('#reply_tab').click(function() { - $(this).removeClass('tell'); - }); - - $('#note_tab').click(function() { - if($('#response').val() != '') { - $('#reply_tab').addClass('tell'); - } - }); - - $('#response_options ul.tabs li a').click(function(e) { - $("#msg_error, #msg_notice, #msg_warning").fadeOut(); - }); - - $('#toggle_ticket_thread, #toggle_notes, .show_notes').click(function(e) { - e.preventDefault(); - $('#threads a').removeClass('active'); - - if($(this).attr('id') == 'toggle_ticket_thread') { - $('#ticket_notes').hide(); - $('#ticket_thread').show(); - $('#toggle_ticket_thread').addClass('active'); - $('#reply_tab').removeClass('tell').click(); - } else { - $('#ticket_thread').hide(); - $('#ticket_notes').show(); - $('#toggle_notes').addClass('active'); - $('#note_tab').click(); - if($('#response').val() != '') { - $('#reply_tab').addClass('tell'); - } - } - }); - //Start watching the form for activity. autoLock.Init(); @@ -377,15 +339,16 @@ var ticket_onload = function($) { +$(this).attr('href').substr(1) +'?_uid='+new Date().getTime(); var $redirect = $(this).data('href'); + var $options = $(this).data('dialog'); $.dialog(url, [201], function (xhr) { window.location.href = $redirect ? $redirect : window.location.href; - }); + }, $options); return false; }); - $(document).on('change', 'form#reply select#emailreply', function(e) { - var $cc = $('form#reply tbody#cc_sec'); + $(document).on('change', 'form[name=reply] select#emailreply', function(e) { + var $cc = $('form[name=reply] tbody#cc_sec'); if($(this).val() == 0) $cc.hide(); else diff --git a/scp/js/tips.js b/scp/js/tips.js index 93f4f14c75d49d1c151e27ffc787e1abb42f3078..127d81db312a7d37a0b56567a48ca69b89c1b00b 100644 --- a/scp/js/tips.js +++ b/scp/js/tips.js @@ -1,6 +1,10 @@ jQuery(function() { var showtip = function (url, elem,xoffset) { + // If element is no longer visible + if (!elem.is(':visible')) + return; + var pos = elem.offset(); var y_pos = pos.top - 12; var x_pos = pos.left + (xoffset || (elem.width() + 16)); @@ -9,20 +13,25 @@ jQuery(function() { var tip_box = $('<div>').addClass('tip_box'); var tip_shadow = $('<div>').addClass('tip_shadow'); var tip_content = $('<div>').addClass('tip_content').load(url, function() { - tip_content.prepend('<a href="#" class="tip_close"><i class="icon-remove-circle"></i></a>').append(tip_arrow); - var width = $(window).width(), - rtl = $('html').hasClass('rtl'), - size = tip_content.outerWidth(), - left = the_tip.position().left, - left_room = left - size, - right_room = width - size - left, - flip = rtl - ? (left_room > 0 && left_room > right_room) - : (right_room < 0 && left_room > right_room); - if (flip) { - the_tip.css({'left':x_pos-tip_content.outerWidth()-elem.width()-32+'px'}); - tip_box.addClass('right'); - tip_arrow.addClass('flip-x'); + if (elem.is(':visible')) { + tip_content.prepend('<a href="#" class="tip_close"><i class="icon-remove-circle"></i></a>').append(tip_arrow); + var width = $(window).width(), + rtl = $('html').hasClass('rtl'), + size = tip_content.outerWidth(), + left = the_tip.position().left, + left_room = left - size, + right_room = width - size - left, + flip = rtl + ? (left_room > 0 && left_room > right_room) + : (right_room < 0 && left_room > right_room); + if (flip) { + the_tip.css({'left':x_pos-tip_content.outerWidth()-elem.width()-32+'px'}); + tip_box.addClass('right'); + tip_arrow.addClass('flip-x'); + } + } else { + // Self close if the element is gone + $('.tip_box').remove(); } }); @@ -31,11 +40,16 @@ jQuery(function() { "top":y_pos + "px", "left":x_pos + "px" }).addClass(elem.data('id')); + + // Close any open tips $('.tip_box').remove(); - $('body').append(the_tip.hide().fadeIn()); - $('.' + elem.data('id') + ' .tip_shadow').css({ - "height":$('.' + elem.data('id')).height() + 5 - }); + // Only show the tip if the element is still visible. + if (elem.is(':visible')) { + $('body').append(the_tip.hide().fadeIn()); + $('.' + elem.data('id') + ' .tip_shadow').css({ + "height":$('.' + elem.data('id')).height() + 5 + }); + } }, getHelpTips = (function() { var dfd, cache = {}; @@ -219,16 +233,15 @@ jQuery(function() { }); - //Ticket preview - $('.ticketPreview').live('mouseover', function(e) { + // Tooltip preview + $('.preview').live('mouseover', function(e) { e.preventDefault(); var elem = $(this); - var vars = elem.attr('href').split('='); - var url = 'ajax.php/tickets/'+vars[1]+'/preview'; - var id='t'+vars[1]; + var url = 'ajax.php/'+elem.data('preview').substr(1); + // TODO - hash url to integer and use it as id. + var id= url.match(/\d/g).join(""); var xoffset = 80; - elem.data('timer', 0); if(!elem.data('id')) { elem.data('id', id); @@ -245,32 +258,6 @@ jQuery(function() { clearTimeout($(this).data('timer')); }); - //User preview - $('.userPreview').live('mouseover', function(e) { - e.preventDefault(); - var elem = $(this); - - var vars = elem.attr('href').split('='); - var url = 'ajax.php/users/'+vars[1]+'/preview'; - var id='u'+vars[1]; - var xoffset = 80; - - elem.data('timer', 0); - if(!elem.data('id')) { - elem.data('id', id); - if(e.type=='mouseover') { - /* wait about 1 sec - before showing the tip - mouseout kills the timeout*/ - elem.data('timer',setTimeout(function() { showtip(url,elem,xoffset);},750)) - }else{ - clearTimeout(elem.data('timer')); - showtip(url, elem, xoffset); - } - } - }).live('mouseout', function(e) { - $(this).data('id', 0); - clearTimeout($(this).data('timer')); - }); - $('body') .delegate('.tip_close', 'click', function(e) { e.preventDefault(); diff --git a/scp/tickets.php b/scp/tickets.php index ec7a3600a3c78f7e22406acff78d95aaa3166b39..f5019127bbec0e3cbe2c2d587b1ec16ea74e0b82 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -58,7 +58,7 @@ if($_POST && !$errors): //More coffee please. $errors=array(); $lock=$ticket->getLock(); //Ticket lock if any - $role = $thisstaff->getRole($ticket->getDeptId); + $role = $thisstaff->getRole($ticket->getDeptId()); switch(strtolower($_POST['a'])): case 'reply': if(!$role || !$role->canPostReply()) diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index 94492a21ade51447ee70c52fec850b15e253383d..9cc9a40035c59a836bddd2d926bf67023a1ef544 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -17,12 +17,14 @@ CREATE TABLE `%TABLE_PREFIX%api_key` ( DROP TABLE IF EXISTS `%TABLE_PREFIX%attachment`; CREATE TABLE `%TABLE_PREFIX%attachment` ( + `id` int(10) unsigned NOT NULL auto_increment, `object_id` int(11) unsigned NOT NULL, `type` char(1) NOT NULL, `file_id` int(11) unsigned NOT NULL, `inline` tinyint(1) unsigned NOT NULL DEFAULT '0', `lang` varchar(16), - PRIMARY KEY (`object_id`,`file_id`,`type`) + PRIMARY KEY (`id`), + UNIQUE KEY `file-type` (`object_id`,`file_id`,`type`) ) DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `%TABLE_PREFIX%faq`; @@ -198,6 +200,7 @@ CREATE TABLE `%TABLE_PREFIX%list_items` ( DROP TABLE IF EXISTS `%TABLE_PREFIX%department`; CREATE TABLE `%TABLE_PREFIX%department` ( `id` int(11) unsigned NOT NULL auto_increment, + `pid` int(11) unsigned default NULL, `tpl_id` int(10) unsigned NOT NULL default '0', `sla_id` int(10) unsigned NOT NULL default '0', `email_id` int(10) unsigned NOT NULL default '0', @@ -209,6 +212,7 @@ CREATE TABLE `%TABLE_PREFIX%department` ( `group_membership` tinyint(1) NOT NULL default '0', `ticket_auto_response` tinyint(1) NOT NULL default '1', `message_auto_response` tinyint(1) NOT NULL default '0', + `path` varchar(128) NOT NULL default '/', `updated` datetime NOT NULL, `created` datetime NOT NULL, PRIMARY KEY (`id`), @@ -414,6 +418,18 @@ CREATE TABLE `%TABLE_PREFIX%role` ( UNIQUE KEY `name` (`name`) ) DEFAULT CHARSET=utf8; +DROP TABLE IF EXISTS `%TABLE_PREFIX%role`; +CREATE TABLE `%TABLE_PREFIX%role` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `flags` int(10) unsigned NOT NULL DEFAULT '1', + `name` varchar(64) DEFAULT NULL, + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) DEFAULT CHARSET=utf8; + DROP TABLE IF EXISTS `%TABLE_PREFIX%group_dept_access`; CREATE TABLE `%TABLE_PREFIX%group_dept_access` ( `group_id` int(10) unsigned NOT NULL default '0', @@ -597,6 +613,52 @@ CREATE TABLE `%TABLE_PREFIX%team_member` ( PRIMARY KEY (`team_id`,`staff_id`) ) DEFAULT CHARSET=utf8; +DROP TABLE IF EXISTS `%TABLE_PREFIX%thread`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%thread` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `object_id` int(11) unsigned NOT NULL, + `object_type` char(1) NOT NULL, + `extra` text, + `created` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `object_id` (`object_id`), + KEY `object_type` (`object_type`) +) DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%thread_entry`; +CREATE TABLE `%TABLE_PREFIX%thread_entry` ( + `id` int(11) unsigned NOT NULL auto_increment, + `pid` int(11) unsigned NOT NULL default '0', + `thread_id` int(11) unsigned NOT NULL default '0', + `staff_id` int(11) unsigned NOT NULL default '0', + `user_id` int(11) unsigned not null default 0, + `type` char(1) NOT NULL default '', + `poster` varchar(128) NOT NULL default '', + `source` varchar(32) NOT NULL default '', + `title` varchar(255), + `body` text NOT NULL, + `format` varchar(16) NOT NULL default 'html', + `ip_address` varchar(64) NOT NULL default '', + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `pid` (`pid`), + KEY `thread_id` (`thread_id`), + KEY `staff_id` (`staff_id`), + KEY `type` (`type`) +) DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%thread_entry_email`; +CREATE TABLE `%TABLE_PREFIX%thread_entry_email` ( + `id` int(11) unsigned NOT NULL auto_increment, + `thread_entry_id` int(11) unsigned NOT NULL, + `mid` varchar(255) NOT NULL, + `headers` text, + PRIMARY KEY (`id`), + KEY `thread_entry_id` (`thread_entry_id`), + KEY `mid` (`mid`) +) DEFAULT CHARSET=utf8; + DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket`; CREATE TABLE `%TABLE_PREFIX%ticket` ( `ticket_id` int(11) unsigned NOT NULL auto_increment, @@ -637,20 +699,6 @@ CREATE TABLE `%TABLE_PREFIX%ticket` ( KEY `sla_id` (`sla_id`) ) DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_attachment`; -CREATE TABLE `%TABLE_PREFIX%ticket_attachment` ( - `attach_id` int(11) unsigned NOT NULL auto_increment, - `ticket_id` int(11) unsigned NOT NULL default '0', - `file_id` int(10) unsigned NOT NULL default '0', - `ref_id` int(11) unsigned NOT NULL default '0', - `inline` tinyint(1) NOT NULL default '0', - `created` datetime NOT NULL, - PRIMARY KEY (`attach_id`), - KEY `ticket_id` (`ticket_id`), - KEY `ref_id` (`ref_id`), - KEY `file_id` (`file_id`) -) DEFAULT CHARSET=utf8; - DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_lock`; CREATE TABLE `%TABLE_PREFIX%ticket_lock` ( `lock_id` int(11) unsigned NOT NULL auto_increment, @@ -663,16 +711,6 @@ CREATE TABLE `%TABLE_PREFIX%ticket_lock` ( KEY `staff_id` (`staff_id`) ) DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_email_info`; -CREATE TABLE `%TABLE_PREFIX%ticket_email_info` ( - `id` int(11) unsigned NOT NULL auto_increment, - `thread_id` int(11) unsigned NOT NULL, - `email_mid` varchar(255) NOT NULL, - `headers` text, - PRIMARY KEY (`id`), - KEY `email_mid` (`email_mid`) -) DEFAULT CHARSET=utf8; - DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_event`; CREATE TABLE `%TABLE_PREFIX%ticket_event` ( `ticket_id` int(11) unsigned NOT NULL default '0', @@ -719,28 +757,6 @@ CREATE TABLE `%TABLE_PREFIX%ticket_priority` ( KEY `ispublic` (`ispublic`) ) DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_thread`; -CREATE TABLE `%TABLE_PREFIX%ticket_thread` ( - `id` int(11) unsigned NOT NULL auto_increment, - `pid` int(11) unsigned NOT NULL default '0', - `ticket_id` int(11) unsigned NOT NULL default '0', - `staff_id` int(11) unsigned NOT NULL default '0', - `user_id` int(11) unsigned not null default 0, - `thread_type` enum('M','R','N') NOT NULL, - `poster` varchar(128) NOT NULL default '', - `source` varchar(32) NOT NULL default '', - `title` varchar(255), - `body` mediumtext NOT NULL, - `format` varchar(16) NOT NULL default 'html', - `ip_address` varchar(64) NOT NULL default '', - `created` datetime NOT NULL, - `updated` datetime NOT NULL, - PRIMARY KEY (`id`), - KEY `ticket_id` (`ticket_id`), - KEY `staff_id` (`staff_id`), - KEY `pid` (`pid`) -) DEFAULT CHARSET=utf8; - CREATE TABLE `%TABLE_PREFIX%ticket_collaborator` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `isactive` tinyint(1) NOT NULL DEFAULT '1', @@ -754,6 +770,29 @@ CREATE TABLE `%TABLE_PREFIX%ticket_collaborator` ( UNIQUE KEY `collab` (`ticket_id`,`user_id`) ) DEFAULT CHARSET=utf8; +DROP TABLE IF EXISTS `%TABLE_PREFIX%task`; +CREATE TABLE `%TABLE_PREFIX%task` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `object_id` int(11) NOT NULL DEFAULT '0', + `object_type` char(1) NOT NULL, + `number` varchar(20) DEFAULT NULL, + `dept_id` int(10) unsigned NOT NULL DEFAULT '0', + `sla_id` int(10) unsigned NOT NULL DEFAULT '0', + `staff_id` int(10) unsigned NOT NULL DEFAULT '0', + `team_id` int(10) unsigned NOT NULL DEFAULT '0', + `flags` int(10) unsigned NOT NULL DEFAULT '0', + `duedate` datetime DEFAULT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `dept_id` (`dept_id`), + KEY `staff_id` (`staff_id`), + KEY `team_id` (`team_id`), + KEY `created` (`created`), + KEY `sla_id` (`sla_id`), + KEY `object` (`object_id`,`object_type`) +) DEFAULT CHARSET=utf8; + -- pages CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%content` ( `id` int(10) unsigned NOT NULL auto_increment,