diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 1e95dde3ec1c033f51fb70b13456739aafea7cfb..d7f53279963b4272686c7d50a1cf19e0b67bb4bd 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -192,7 +192,6 @@ class TicketsAjaxAPI extends AjaxController { include STAFFINC_DIR . 'templates/ticket-preview.tmpl.php'; } - function viewUser($tid) { global $thisstaff; diff --git a/include/class.collaborator.php b/include/class.collaborator.php index 205b5200b151911c757611483fae9560f041bb27..6bc891e5fd9fd53d19789faac4de813fc2ab1dfc 100644 --- a/include/class.collaborator.php +++ b/include/class.collaborator.php @@ -35,8 +35,10 @@ implements EmailContact, ITicketUser { ); function __toString() { - return Format::htmlchars(sprintf('%s <%s>', $this->getName(), - $this->getEmail())); + return Format::htmlchars($this->toString()); + } + function toString() { + return sprintf('%s <%s>', $this->getName(), $this->getEmail()); } function getId() { diff --git a/include/class.orm.php b/include/class.orm.php index 57029871265b38038f32d5726038a724d39d3de0..b7250ee94a2a18a75cb2cb76e9991b6f673585bf 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -193,7 +193,8 @@ class VerySimpleModel { $fkey[$F ?: $_klas] = ($local[0] == "'") ? trim($local, "'") : $this->ht[$local]; } - $v = $this->ht[$field] = new InstrumentedList( + $manager = @$j['class'] ?: 'InstrumentedList'; + $v = $this->ht[$field] = new $manager( // Send Model, [Foriegn-Field => Local-Id] array($class, $fkey) ); diff --git a/include/class.thread.php b/include/class.thread.php index 75d5a14a396bf1830b0708f89f6a6d0f7caeb72d..285bd9f163144b93cb69794d6639e4ee4d40ac4b 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -42,6 +42,10 @@ class Thread extends VerySimpleModel { 'entries' => array( 'reverse' => 'ThreadEntry.thread', ), + 'events' => array( + 'reverse' => 'ThreadEvent.thread', + 'class' => 'ThreadEvents', + ), ), ); @@ -198,6 +202,7 @@ class Thread extends VerySimpleModel { if ($type && is_array($type)) $entries->filter(array('type__in' => $type)); + $events = $this->getEvents(); include STAFFINC_DIR . 'templates/thread-entries.tmpl.php'; } @@ -205,6 +210,10 @@ class Thread extends VerySimpleModel { return ThreadEntry::lookup($id, $this->getId()); } + function getEvents() { + return $this->events; + } + /** * postEmail * @@ -1406,6 +1415,244 @@ implements TemplateVariable { RolePermission::register(/* @trans */ 'Tickets', ThreadEntry::getPermissions()); +class ThreadEvent extends VerySimpleModel { + static $meta = array( + 'table' => TICKET_EVENT_TABLE, + 'pk' => array('id'), + 'joins' => array( + 'thread' => array( + 'constraint' => array('thread_id' => 'Thread.id'), + ), + 'staff' => array( + 'constraint' => array( + 'uid' => 'Staff.staff_id', + ), + 'null' => true, + ), + 'user' => array( + 'constraint' => array( + 'uid' => 'User.id', + ), + 'null' => true, + ), + 'dept' => array( + 'constraint' => array( + 'dept_id' => 'Dept.id', + ), + 'null' => true, + ), + ), + ); + + // Valid events for database storage + const ASSIGNED = 'assigned'; + const CLOSED = 'closed'; + const CREATED = 'created'; + const COLLAB = 'collab'; + const EDITED = 'edited'; + const ERROR = 'error'; + const OVERDUE = 'overdue'; + const REOPENED = 'reopened'; + const STATUS = 'status'; + const TRANFERRED = 'transferred'; + const VIEWED = 'viewed'; + + const MODE_STAFF = 1; + const MODE_CLIENT = 2; + + var $_data; + + function getUserName() { + if ($this->uid && $this->uid_type == 'S') + return $this->staff->getName(); + if ($this->uid && $this->uid_type == 'U') + return $this->user->getName(); + return $this->username; + } + + function getIcon() { + $icons = array( + 'assigned' => 'hand-right', + 'collab' => 'group', + 'created' => 'magic', + 'overdue' => 'time', + ); + return @$icons[$this->state] ?: 'chevron-sign-right'; + } + + function getDescription() { + static $descs; + if (!isset($descs)) + $descs = array( + 'assigned:staff' => __('<b>{username}</b> assigned this to <strong>{<Staff>data.staff}</strong> {timestamp}'), + 'assigned:team' => __('<b>{username}</b> assigned this to <strong>{<Team>data.team}</strong> {timestamp}'), + 'assigned:claim' => __('<b>{username}</b> claimed this {timestamp}'), + 'collab:org' => __('Collaborators for {<Organization>data.org} organization added'), + 'collab:del' => function($evt) { + $data = $evt->getData(); + $base = __('<b>{username}</b> removed %s from the collaborators.'); + return $data['del'] + ? Format::htmlchars(sprintf($base, implode(', ', $data['del']))) + : 'somebody'; + }, + 'collab:add' => function($evt) { + $data = $evt->getData(); + $base = __('<b>{username}</b> added %s as collaborators {timestamp}'); + $collabs = array(); + if ($data['add']) { + foreach ($data['add'] as $c) { + $collabs[] = '<b>'.Format::htmlchars($c).'</b>'; + } + } + return $collabs + ? sprintf($base, implode(', ', $collabs)) + : 'somebody'; + }, + 'created' => __('<strong>Created</strong> by <b>{username}</b> {timestamp}'), + 'edited:owner' => __('<b>{username}</b> changed ownership to {<User>data.owner} {timestamp}'), + 'edited:status' => __('<b>{username}</b> changed the status to <strong>{<TicketStatus>data.status}</strong> {timestamp}'), + 'overdue' => __('Flagged as overdue by the system {timestamp}'), + 'transferred' => __('<b>{username}</b> transferred this to {dept} {timestamp}'), + ); + $self = $this; + $data = $this->getData(); + $state = $this->state; + if (is_array($data)) { + foreach (array_keys($data) as $k) + if (isset($descs[$state . ':' . $k])) + $state .= ':' . $k; + } + $description = $descs[$state]; + if (is_callable($description)) + $description = $description($this); + + return preg_replace_callback('/\{(<(?P<type>([^>]+))>)?(?P<key>[^}.]+)(\.(?P<data>[^}]+))?\}/', + function ($m) use ($self) { + switch ($m['key']) { + case 'username': + return $self->getUserName(); + case 'timestamp': + return sprintf('<time class="relative" datetime="%s" title="%s">%s</time>', + date(DateTime::W3C, Misc::db2gmtime($self->timestamp)), + Format::daydatetime($self->timestamp), + Format::relativeTime(Misc::db2gmtime($self->timestamp)) + ); + case 'dept': + if ($dept = $this->getDept()) + return $dept->getLocalName(); + case 'data': + $val = $self->getData($m['data']); + if ($m['type'] && class_exists($m['type'])) + $val = $m['type']::lookup($val); + return (string) $val; + } + return $m[0]; + }, + $description + ); + } + + function getDept() { + return $this->dept; + } + + function getData($key=false) { + if (!isset($this->_data)) + $this->_data = JsonDataParser::decode($this->data); + return ($key) ? @$this->_data[$key] : $this->_data; + } + + function render($mode) { + if ($mode == self::MODE_STAFF) { + $event = $this; + include STAFFINC_DIR . 'templates/thread-event.tmpl.php'; + } + } + + static function create($ht=false) { + $inst = parent::create($ht); + $inst->timestamp = SqlFunction::NOW(); + + global $thisstaff, $thisclient; + if ($thisstaff) { + $inst->uid_type = 'S'; + $inst->uid = $thisstaff->getId(); + } + else if ($thisclient) { + $inst->uid_type = 'U'; + $inst->uid = $thisclient->getId(); + } + + return $inst; + } + + static function forTicket($ticket, $state) { + $inst = static::create(array( + 'staff_id' => $ticket->getStaffId(), + 'team_id' => $ticket->getTeamId(), + 'dept_id' => $ticket->getDeptId(), + 'topic_id' => $ticket->getTopicId(), + )); + if (!isset($inst->uid_type) && $state == self::CREATED) { + $inst->uid_type = 'U'; + $inst->uid = $ticket->getOwnerId(); + } + return $inst; + } +} + +class ThreadEvents extends InstrumentedList { + function annul($event) { + $this->queryset + ->filter(array('state' => $event)) + ->update(array('annulled' => 1)); + } + + function log($object, $state, $data=null, $annul=null, $username=null) { + if ($object instanceof Ticket) + $event = ThreadEvent::forTicket($object, $state); + else + $event = ThreadEvent::create(); + + # Annul previous entries if requested (for instance, reopening a + # ticket will annul an 'closed' entry). This will be useful to + # easily prevent repeated statistics. + if ($annul) { + $this->annul($annul); + } + + if ($username === null) { + if ($thisstaff) { + $username = $thisstaff->getUserName(); + } + else if ($thisclient) { + if ($thisclient->hasAccount) + $username = $thisclient->getAccount()->getUserName(); + if (!$username) + $username = $thisclient->getEmail(); + } + else { + # XXX: Security Violation ? + $username = 'SYSTEM'; + } + } + $event->username = $username; + $event->state = $state; + + if ($data) { + if (is_array($data)) + $data = JsonDataEncoder::encode($data); + if (!is_string($data)) + throw new InvalidArgumentException('Data must be string or array'); + $event->data = $data; + } + + $this->add($event); + + // Save event immediately + return $event->save(); + } +} class ThreadEntryBody /* extends SplString */ { diff --git a/include/class.ticket.php b/include/class.ticket.php index 605758114102bcc1dfe959abec689b933a8e4fb9..0f9ed9386d066a649aaeae6c3b1067bb4d467474 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -943,6 +943,8 @@ implements RestrictedAccess, Threadable, TemplateVariable { $this->collaborators = null; $this->recipients = null; + $this->logEvent('collab', array('add' => array($c->toString()))); + return $c; } @@ -959,11 +961,10 @@ implements RestrictedAccess, Threadable, TemplateVariable { if (($c=Collaborator::lookup($cid)) && $c->getTicketId() == $this->getId() && $c->delete()) - $collabs[] = $c; + $collabs[] = (string) $c; } - $this->logNote(_S('Collaborators Removed'), - implode("<br>", $collabs), $thisstaff, false); + $this->logEvent('collab', array('del' => $collabs)); } //statuses @@ -1164,6 +1165,7 @@ implements RestrictedAccess, Threadable, TemplateVariable { } } + $hadStatus = $this->getStatusId(); if ($this->getStatusId() == $status->getId()) return true; @@ -1196,7 +1198,7 @@ implements RestrictedAccess, Threadable, TemplateVariable { if ($this->isClosed()) { $sql .= ',closed=NULL, lastupdate=NOW(), reopened=NOW() '; $ecb = function ($t) { - $t->logEvent('reopened', 'closed'); + $t->logEvent('reopened', false, 'closed'); }; } @@ -1228,12 +1230,15 @@ implements RestrictedAccess, Threadable, TemplateVariable { $note .= sprintf('<hr>%s', $comments); // Send out alerts if comments are included $alert = true; + $this->logNote(__('Status Changed'), $note, $thisstaff, $alert); } - - $this->logNote(__('Status Changed'), $note, $thisstaff, $alert); } // Log events via callback - if ($ecb) $ecb($this); + if ($ecb) + $ecb($this); + elseif ($hadStatus) + // Don't log the initial status change + $this->logEvent('edited', array('status' => $status->getId())); return true; } @@ -1662,13 +1667,16 @@ implements RestrictedAccess, Threadable, TemplateVariable { $this->reload(); + $user_comments = (bool) $comments; $comments = $comments ?: _S('Ticket assignment'); $assigner = $thisstaff ?: _S('SYSTEM (Auto Assignment)'); //Log an internal note - no alerts on the internal note. - $note = $this->logNote( - sprintf(_S('Ticket Assigned to %s'), $assignee->getName()), - $comments, $assigner, false); + if ($user_comments) { + $note = $this->logNote( + sprintf(_S('Ticket Assigned to %s'), $assignee->getName()), + $comments, $assigner, false); + } //See if we need to send alerts if(!$alert || !$cfg->alertONAssignment()) return true; //No alerts! @@ -1971,8 +1979,11 @@ implements RestrictedAccess, Threadable, TemplateVariable { /*** log the transfer comments as internal note - with alerts disabled - ***/ $title=sprintf(_S('Ticket transferred from %1$s to %2$s'), $currentDept, $this->getDeptName()); - $comments=$comments?$comments:$title; - $note = $this->logNote($title, $comments, $thisstaff, false); + + if ($comments) { + $note = $this->logNote($title, $comments, $thisstaff, false); + } + $comments = $comments ?: $title; $this->logEvent('transferred'); @@ -2004,11 +2015,13 @@ implements RestrictedAccess, Threadable, TemplateVariable { if($cfg->alertDeptManagerONTransfer() && $dept && ($manager=$dept->getManager())) $recipients[]= $manager; - $sentlist=array(); - $options = array( - 'inreplyto'=>$note->getEmailMessageId(), - 'references'=>$note->getEmailReferences(), - 'thread'=>$note); + $sentlist = $options = array(); + if ($note) { + $options += array( + 'inreplyto'=>$note->getEmailMessageId(), + 'references'=>$note->getEmailReferences(), + 'thread'=>$note); + } foreach( $recipients as $k=>$staff) { if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = $this->replaceVars($msg, array('recipient' => $staff)); @@ -2030,9 +2043,7 @@ implements RestrictedAccess, Threadable, TemplateVariable { if ($dept->assignMembersOnly() && !$dept->isMember($thisstaff)) return false; - $comments = sprintf(_S('Ticket claimed by %s'), $thisstaff->getName()); - - return $this->assignToStaff($thisstaff->getId(), $comments, false); + return $this->assignToStaff($thisstaff->getId(), null, false); } function assignToStaff($staff, $note, $alert=true) { @@ -2044,7 +2055,14 @@ implements RestrictedAccess, Threadable, TemplateVariable { return false; $this->onAssign($staff, $note, $alert); - $this->logEvent('assigned'); + + global $thisstaff; + $data = array(); + if ($staff->getId() == $thisstaff->getId()) + $data['claim'] = true; + else + $data['staff'] = $staff->getId(); + $this->logEvent('assigned', $data); return true; } @@ -2063,7 +2081,7 @@ implements RestrictedAccess, Threadable, TemplateVariable { $this->setStaffId(0); $this->onAssign($team, $note, $alert); - $this->logEvent('assigned'); + $this->logEvent('assigned', array('team' => $team->getId())); return true; } @@ -2134,18 +2152,15 @@ implements RestrictedAccess, Threadable, TemplateVariable { $this->collaborators = null; $this->recipients = null; - //Log an internal note - $note = sprintf(_S('%s changed ticket ownership to %s'), - $thisstaff->getName(), $user->getName()); - - //Remove the new owner from list of collaborators + // Remove the new owner from list of collaborators $c = Collaborator::lookup(array( - 'user_id' => $user->getId(), - 'thread_id' => $this->getThreadId())); - if ($c && $c->delete()) - $note.= ' '._S('(removed as collaborator)'); + 'user_id' => $user->getId(), + 'thread_id' => $this->getThreadId() + )); + if ($c) + $c->delete(); - $this->logNote('Ticket ownership changed', $note); + $this->logEvent('edited', array('owner' => $user->getId())); return true; } @@ -2184,18 +2199,11 @@ implements RestrictedAccess, Threadable, TemplateVariable { if (($user=User::fromVars($recipient))) if ($c=$this->addCollaborator($user, $info, $errors)) - $collabs[] = sprintf('%s%s', - (string) $c, - $recipient['source'] - ? " ".sprintf(_S('via %s'), $recipient['source']) - : '' - ); + $collabs[] = array((string)$c, $recipient['source']); } //TODO: Can collaborators add others? if ($collabs) { - //TODO: Change EndUser to name of user. - $this->logNote(_S('Collaborators added by end user'), - implode("<br>", $collabs), _S('End User'), false); + $this->logEvent('collab', array('add' => $collabs)); } } @@ -2411,31 +2419,8 @@ implements RestrictedAccess, Threadable, TemplateVariable { } // History log -- used for statistics generation (pretty reports) - function logEvent($state, $annul=null, $staff=null) { - global $thisstaff; - - if ($staff === null) { - if ($thisstaff) $staff=$thisstaff->getUserName(); - else $staff='SYSTEM'; # XXX: Security Violation ? - } - # Annul previous entries if requested (for instance, reopening a - # ticket will annul an 'closed' entry). This will be useful to - # easily prevent repeated statistics. - if ($annul) { - db_query('UPDATE '.TICKET_EVENT_TABLE.' SET annulled=1' - .' WHERE ticket_id='.db_input($this->getId()) - .' AND state='.db_input($annul)); - } - - return db_query('INSERT INTO '.TICKET_EVENT_TABLE - .' SET ticket_id='.db_input($this->getId()) - .', staff_id='.db_input($this->getStaffId()) - .', team_id='.db_input($this->getTeamId()) - .', dept_id='.db_input($this->getDeptId()) - .', topic_id='.db_input($this->getTopicId()) - .', timestamp=NOW(), state='.db_input($state) - .', staff='.db_input($staff)) - && db_affected_rows() == 1; + function logEvent($state, $data=null, $annul=null, $staff=null) { + $this->getThread()->getEvents()->log($this, $state, $data, $annul, $staff); } //Insert Internal Notes @@ -3262,10 +3247,7 @@ implements RestrictedAccess, Threadable, TemplateVariable { } //TODO: Can collaborators add others? if ($collabs) { - //TODO: Change EndUser to name of user. - $ticket->logNote(sprintf(_S('Collaborators for %s organization added'), - $org->getName()), - implode("<br>", $collabs), $org->getName(), false); + $ticket->logEvent('collab', array('org' => $org->getId())); } } @@ -3418,11 +3400,6 @@ implements RestrictedAccess, Threadable, TemplateVariable { } $ticket->logNote(_S('New Ticket'), $vars['note'], $thisstaff, false); } - else { - // Not assignment and no internal note - log activity - $ticket->logActivity(_S('New Ticket by Agent'), - sprintf(_S('Ticket created by agent - %s'), $thisstaff->getName())); - } $ticket->reload(); @@ -3493,9 +3470,8 @@ implements RestrictedAccess, Threadable, TemplateVariable { if(($res=db_query($sql)) && db_num_rows($res)) { while(list($id)=db_fetch_row($res)) { - if(($ticket=Ticket::lookup($id)) && $ticket->markOverdue()) - $ticket->logActivity(_S('Ticket Marked Overdue'), - _S('Ticket flagged as overdue by the system.')); + if ($ticket=Ticket::lookup($id)) + $ticket->markOverdue(); } } else { //TODO: Trigger escalation on already overdue tickets - make sure last overdue event > grace_period. diff --git a/include/class.user.php b/include/class.user.php index 1b4e309d8af92aae4aef4e277ae23df2c0ffb894..02e45ae14b20d5dca88df1c25078c0109ab110ca 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -39,7 +39,7 @@ class UserModel extends VerySimpleModel { static $meta = array( 'table' => USER_TABLE, 'pk' => array('id'), - 'select_related' => array('default_email'), + 'select_related' => array('account', 'default_email'), 'joins' => array( 'emails' => array( 'reverse' => 'UserEmailModel.user', @@ -123,6 +123,9 @@ class UserModel extends VerySimpleModel { return $this->default_email; } + function hasAccount() { + return !is_null($this->account); + } function getAccount() { return $this->account; } diff --git a/include/staff/faq-category.inc.php b/include/staff/faq-category.inc.php index afc5194bb7c08ebfe4ea4caeed74072a5b434db4..b6287bcb9eb3779a9aab6aa5a49dc244683e1856 100644 --- a/include/staff/faq-category.inc.php +++ b/include/staff/faq-category.inc.php @@ -11,7 +11,7 @@ if(!defined('OSTSTAFFINC') || !$category || !$thisstaff) die('Access Denied'); <div> <strong><?php echo $category->getName() ?></strong> <span>(<?php echo $category->isPublic()?__('Public'):__('Internal'); ?>)</span> - <time> <?php echo __('Last updated').' '. Format::daydatetime($category->getUpdateDate()); ?></time> + <time class="faq"> <?php echo __('Last updated').' '. Format::daydatetime($category->getUpdateDate()); ?></time> </div> <div class="cat-desc"> <?php echo Format::display($category->getDescription()); ?> diff --git a/include/staff/templates/thread-entries.tmpl.php b/include/staff/templates/thread-entries.tmpl.php index 2e7da2c389cf2b276b139a5573e040fb0e6590a9..c9febda653f646ecae1030f3fdc188945ca0a3b3 100644 --- a/include/staff/templates/thread-entries.tmpl.php +++ b/include/staff/templates/thread-entries.tmpl.php @@ -1,90 +1,26 @@ <?php -$entryTypes = array('M'=>'message', 'R'=>'response', 'N'=>'note'); -if ($entries) { - foreach ($entries as $entry) { ?> - <table class="thread-entry <?php echo $entryTypes[$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;max-width: 500px" class="faded title truncate"><?php - echo $entry->title; ?></span> - </span> - <div class="pull-right"> -<?php if ($entry->hasActions()) { - $actions = $entry->getActions(); ?> - <span class="action-button pull-right" data-dropdown="#entry-action-more-<?php echo $entry->getId(); ?>"> - <i class="icon-caret-down"></i> - <span ><i class="icon-cog"></i></span> - </span> - <div id="entry-action-more-<?php echo $entry->getId(); ?>" class="action-dropdown anchor-right"> - <ul class="title"> -<?php foreach ($actions as $group => $list) { - foreach ($list as $id => $action) { ?> - <li> - <a class="no-pjax" href="#" onclick="javascript: - <?php echo str_replace('"', '\\"', $action->getJsStub()); ?>; return false;"> - <i class="<?php echo $action->getIcon(); ?>"></i> <?php - echo $action->getName(); - ?></a></li> -<?php } - } ?> - </ul> - </div> -<?php } ?> - <span style="vertical-align:middle"> - <span style="vertical-align:middle;" class="textra"> - <?php if ($entry->flags & ThreadEntry::FLAG_EDITED) { ?> - <span class="label label-bare" title="<?php - echo sprintf(__('Edited on %s by %s'), Format::datetime($entry->updated), 'You'); - ?>"><?php echo __('Edited'); ?></span> - <?php } ?> - </span> - <span style="vertical-align:middle;" - class="tmeta faded title"><?php - echo Format::htmlchars($entry->getName()); ?></span> - </span> - </div> - </th> - </tr> - <tr><td colspan="4" class="thread-body" id="thread-id-<?php - echo $entry->getId(); ?>"><div><?php - echo $entry->getBody()->toHtml(); ?></div></td></tr> - <?php - $urls = null; - if ($entry->has_attachments - && ($urls = $entry->getAttachmentUrls())) { ?> - <tr> - <td class="info" colspan="4"><?php - foreach ($entry->attachments as $A) { - if ($A->inline) continue; - $size = ''; - if ($A->file->size) - $size = sprintf('<em>(%s)</em>', - Format::file_size($A->file->size)); -?> - <a class="Icon file no-pjax" href="<?php echo $A->file->getDownloadUrl(); - ?>" download="<?php echo Format::htmlchars($A->file->name); ?>" - target="_blank"><?php echo Format::htmlchars($A->file->name); - ?></a><?php echo $size;?> -<?php } ?> - </td> - </tr> <?php +$events = $events->order_by('id'); +$events = $events->getIterator(); +$events->rewind(); +$event = $events->current(); + +if (count($entries)) { + foreach ($entries as $entry) { + // Emit all events prior to this entry + while ($event && $event->timestamp <= $entry->created) { + $event->render(ThreadEvent::MODE_STAFF); + $events->next(); + $event = $events->current(); } - if ($urls) { ?> - <script type="text/javascript"> - $('#thread-id-<?php echo $entry->getId(); ?>') - .data('urls', <?php - echo JsonDataEncoder::encode($urls); ?>) - .data('id', <?php echo $entry->getId(); ?>); - </script> -<?php - } ?> - </table> - <?php + include STAFFINC_DIR . 'templates/thread-entry.tmpl.php'; + } + // Emit all other events + while ($event) { + $event->render(ThreadEvent::MODE_STAFF); + $events->next(); + $event = $events->current(); } -} else { +} +else { echo '<p><em>'.__('No entries have been posted to this thread.').'</em></p>'; -}?> +} diff --git a/include/staff/templates/thread-entry.tmpl.php b/include/staff/templates/thread-entry.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..d24af728b0cf1608412a8391f763406f54771703 --- /dev/null +++ b/include/staff/templates/thread-entry.tmpl.php @@ -0,0 +1,86 @@ +<?php +$entryTypes = array('M'=>'message', 'R'=>'response', 'N'=>'note'); +?> + +<table class="thread-entry <?php echo $entryTypes[$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;max-width: 500px" class="faded title truncate"><?php + echo $entry->title; ?></span> + </span> + <div class="pull-right"> +<?php if ($entry->hasActions()) { + $actions = $entry->getActions(); ?> + <span class="action-button pull-right" data-dropdown="#entry-action-more-<?php echo $entry->getId(); ?>"> + <i class="icon-caret-down"></i> + <span ><i class="icon-cog"></i></span> + </span> + <div id="entry-action-more-<?php echo $entry->getId(); ?>" class="action-dropdown anchor-right"> + <ul class="title"> +<?php foreach ($actions as $group => $list) { + foreach ($list as $id => $action) { ?> + <li> + <a class="no-pjax" href="#" onclick="javascript: + <?php echo str_replace('"', '\\"', $action->getJsStub()); ?>; return false;"> + <i class="<?php echo $action->getIcon(); ?>"></i> <?php + echo $action->getName(); + ?></a></li> +<?php } + } ?> + </ul> + </div> +<?php } ?> + <span style="vertical-align:middle"> + <span style="vertical-align:middle;" class="textra"> + <?php if ($entry->flags & ThreadEntry::FLAG_EDITED) { ?> + <span class="label label-bare" title="<?php + echo sprintf(__('Edited on %s by %s'), Format::datetime($entry->updated), 'You'); + ?>"><?php echo __('Edited'); ?></span> + <?php } ?> + </span> + <span style="vertical-align:middle;" + class="tmeta faded title"><?php + echo Format::htmlchars($entry->getName()); ?></span> + </span> + </div> + </th> + </tr> + <tr><td colspan="4" class="thread-body" id="thread-id-<?php + echo $entry->getId(); ?>"><div><?php + echo $entry->getBody()->toHtml(); ?></div></td></tr> + <?php + $urls = null; + if ($entry->has_attachments + && ($urls = $entry->getAttachmentUrls())) { ?> + <tr> + <td class="info" colspan="4"><?php + foreach ($entry->attachments as $A) { + if ($A->inline) continue; + $size = ''; + if ($A->file->size) + $size = sprintf('<em>(%s)</em>', + Format::file_size($A->file->size)); +?> + <a class="Icon file no-pjax" href="<?php echo $A->file->getDownloadUrl(); + ?>" download="<?php echo Format::htmlchars($A->file->name); ?>" + target="_blank"><?php echo Format::htmlchars($A->file->name); + ?></a><?php echo $size;?> +<?php } ?> + </td> + </tr> <?php + } + if ($urls) { ?> + <script type="text/javascript"> + $('#thread-id-<?php echo $entry->getId(); ?>') + .data('urls', <?php + echo JsonDataEncoder::encode($urls); ?>) + .data('id', <?php echo $entry->getId(); ?>); + </script> +<?php + } ?> +</table> + diff --git a/include/staff/templates/thread-event.tmpl.php b/include/staff/templates/thread-event.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..8bbf70e5c95b19b0aed8da4629bc4679c7aaa161 --- /dev/null +++ b/include/staff/templates/thread-event.tmpl.php @@ -0,0 +1,8 @@ +<div class="thread-event"> + <span class="type-icon"> + <i class="faded icon-<?php echo $event->getIcon(); ?>"></i> + </span> + <span class="faded" style="display:inline-block"> + <?php echo $event->getDescription(); ?> + </span> +</div> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 74407ea1952c951da73f9345baa0dc6fe88ddb5a..baf81ca4927c0f88ac121a4ca1763601b2f4c0f5 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -422,7 +422,7 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) { <?php $tcount = $ticket->getThreadEntries($types)->count(); ?> -<ul class="tabs threads" id="ticket_tabs" > +<ul class="tabs clean 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 @@ -437,6 +437,7 @@ $tcount = $ticket->getThreadEntries($types)->count(); <?php $ticket->getThread()->render(array('M', 'R', 'N')); ?> + </div> <div class="clear" style="padding-bottom:10px;"></div> <?php if($errors['err']) { ?> <div id="msg_error"><?php echo $errors['err']; ?></div> diff --git a/scp/css/scp.css b/scp/css/scp.css index 4dd7cef3afd1c4cf0b48512b451375e6146e447f..e2709e74a17034203f0902251e589a903f31af18 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -55,10 +55,21 @@ div#header a { color: #666; color: rgba(0,0,0,0.5); } +.faded b { + color: #333; + color: rgba(0,0,0,0.75); +} +.faded strong { + color: #444; + color: rgba(0,0,0,0.6); +} .faded-more { color: #aaa; color: rgba(0,0,0,0.4); } +time[title]:hover { + text-decoration: underline; +} .small[class^="icon-"], .small[class*=" icon-"] { @@ -912,7 +923,7 @@ table.thread-entry th, #ticket_notes table th { } #response_options { - margin-top:30px; + margin-top:10px; } #response_options > form { @@ -1403,7 +1414,7 @@ h2 > i.help-tip { background-color:#e9f5ff; } -time { +time.faq { display:inline-block; float:right; color:#777; @@ -2217,3 +2228,17 @@ td.indented { .sticky.bar .content { margin: auto; } + +.thread-event { + padding: 15px 5px 5px; +} +.thread-event .type-icon { + border-radius: 8px; + background-color: #f4f4f4; + padding: 5px 8px; + margin-right: 5px; + text-align: center; + display: inline-block; + font-size: 1.2em; + border: 1px solid #eee; +} diff --git a/scp/tickets.php b/scp/tickets.php index 4f0dcec4e3642c8628ed5bc0f743926d746becc2..e78104f0c5f294b8ef3eef02651c034b9918b1df 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -186,14 +186,6 @@ if($_POST && !$errors): $errors['assignId']=__('Ticket already assigned to the team.'); } - //Comments are not required on self-assignment (claim) - if($claim && !$_POST['assign_comments']) - $_POST['assign_comments'] = sprintf(__('Ticket claimed by %s'),$thisstaff->getName()); - elseif(!$_POST['assign_comments']) - $errors['assign_comments'] = __('Assignment comments required'); - elseif(strlen($_POST['assign_comments'])<5) - $errors['assign_comments'] = __('Comment too short'); - if(!$errors && $ticket->assign($_POST['assignId'], $_POST['assign_comments'], !$claim)) { if($claim) { $msg = __('Ticket is NOW assigned to you!');