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&nbsp;%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']); ?>
                 &nbsp;&nbsp;<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="">&mdash; <?php echo __('Top-Level Deptartment'); ?> &mdash;</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>&nbsp;<?php echo $default; ?></td>
+                echo Dept::getNameById($id); ?></a>&nbsp;<?php echo $default; ?></td>
                 <td><?php echo $dept->isPublic() ? __('Public') :'<b>'.__('Private').'</b>'; ?></td>
                 <td>&nbsp;&nbsp;
                     <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">&mdash; <?php echo __('System Default'); ?> &mdash;</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>&nbsp;<?php echo __('Users'); ?></a></li>
-    <li><a id="tickets_tab" href="#tickets"><i
+    <li><a href="#tickets"><i
     class="icon-list-alt"></i>&nbsp;<?php echo __('Tickets'); ?></a></li>
-    <li><a id="notes_tab" href="#notes"><i
+    <li><a href="#notes"><i
     class="icon-pushpin"></i>&nbsp;<?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>&nbsp;<?php echo __('Fields'); ?></a></li>
     <li><a href="#contact-settings"
         ><i class="icon-fixed-width icon-cogs faded"></i>&nbsp;<?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') ?>:&nbsp;</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">*&nbsp;<?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">&nbsp;<?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.='&nbsp;<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>&nbsp;'.__('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">&mdash; '.__('Unassigned').' &mdash;</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') ?>:&nbsp;</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">*&nbsp;<?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.='&nbsp;&nbsp;<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">&mdash; '.__('Unassigned').' &mdash;</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">&mdash; '.__('Unknown').' &mdash;</span>';
+                        ?>
+                    </td>
+                </tr>
+                <?php
+                } ?>
+                <tr>
+                    <th><?php echo __('SLA Plan');?>:</th>
+                    <td><?php echo $sla?Format::htmlchars($sla->getName()):'<span class="faded">&mdash; '.__('None').' &mdash;</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">&mdash; '.__('None').' &mdash;</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">&nbsp;<?php echo $errors['title']; ?></span>
+                    </div>
+                    <div>
+                        <label><strong><?php echo __('Internal Note'); ?></strong><span class='error'>&nbsp;* <?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>
+                        &nbsp;<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>&nbsp;<?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>&nbsp;<?php echo __('Copy Paste'); ?></a></li>
+    <li><a href="#upload"
+        ><i class="icon-fixed-width icon-cloud-upload"></i>&nbsp;<?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>&nbsp;<?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>&nbsp;<?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>&nbsp;<?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>&nbsp;
-                    <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>
                     &nbsp;
                     <?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">&nbsp;</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>&nbsp;".'<i
+                            class="icon-fixed-width icon-comments-alt"></i>&nbsp;';
+                    if ($row['collaborators'])
+                        echo '<i class="icon-fixed-width icon-group faded"></i>&nbsp;';
+                    if ($row['attachments'])
+                        echo '<i class="icon-fixed-width icon-paperclip"></i>&nbsp;';
+                ?>
+            </td>
+            <td><?php echo Format::truncate($task->dept->getName(), 40); ?></td>
+            <td>&nbsp;<?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('&nbsp;(%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>&nbsp;<?php echo __('User Tickets'); ?></a></li>
-    <li><a id="notes_tab" href="#notes"><i
+    <li><a href="#notes"><i
     class="icon-pushpin"></i>&nbsp;<?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>&nbsp;
-                    <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>
                     &nbsp;
                     <?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,