diff --git a/bootstrap.php b/bootstrap.php index b8d9052f2475e58192b9f0e301dbe446e9c52cd0..e6b95d038cf21975249d694467fe9958f019b4ff 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -73,6 +73,7 @@ class Bootstrap { define('USER_ACCOUNT_TABLE',$prefix.'user_account'); define('ORGANIZATION_TABLE', $prefix.'organization'); + define('NOTE_TABLE', $prefix.'note'); define('STAFF_TABLE',$prefix.'staff'); define('TEAM_TABLE',$prefix.'team'); diff --git a/include/ajax.note.php b/include/ajax.note.php new file mode 100644 index 0000000000000000000000000000000000000000..99525a23b84530198751bee7d64c03ae34c5416b --- /dev/null +++ b/include/ajax.note.php @@ -0,0 +1,68 @@ +<?php + +if(!defined('INCLUDE_DIR')) die('!'); + +require_once(INCLUDE_DIR.'class.note.php'); + +class NoteAjaxAPI extends AjaxController { + + function getNote($id) { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, "Login required"); + elseif (!($note = QuickNote::lookup($id))) + Http::response(205, "Note not found"); + + Http::response(200, $note->display()); + } + + function updateNote($id) { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, "Login required"); + elseif (!($note = QuickNote::lookup($id))) + Http::response(205, "Note not found"); + elseif (!isset($_POST['note']) || !$_POST['note']) + Http::response(422, "Send `note` parameter"); + + $note->body = Format::sanitize($_POST['note']); + if (!$note->save()) + Http::response(500, "Unable to save note contents"); + + Http::response(200, $note->display()); + } + + function deleteNote($id) { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, "Login required"); + elseif (!($note = QuickNote::lookup($id))) + Http::response(205, "Note not found"); + elseif (!$note->delete()) + Http::response(500, "Unable to remove note"); + } + + function createNote($ext_id) { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, "Login required"); + elseif (!isset($_POST['note']) || !$_POST['note']) + Http::response(422, "Send `note` parameter"); + elseif (!($note = QuickNote::create(array( + 'staff_id' => $thisstaff->getId(), + 'body' => Format::sanitize($_POST['note']), + 'created' => new SqlFunction('NOW'), + 'ext_id' => $ext_id, + )))) + Http::response(500, "Unable to create new note"); + elseif (!$note->save(true)) + Http::response(500, "Unable to create new note"); + + $show_options = true; + include STAFFINC_DIR . 'templates/note.tmpl.php'; + } +} diff --git a/include/ajax.users.php b/include/ajax.users.php index 2193abf85b952d82682095be6709e38b49bbf45c..1fbe7fe6084de53cae776b41b2da63ef2ae35783 100644 --- a/include/ajax.users.php +++ b/include/ajax.users.php @@ -18,6 +18,7 @@ if(!defined('INCLUDE_DIR')) die('403'); include_once(INCLUDE_DIR.'class.ticket.php'); +require_once INCLUDE_DIR.'class.note.php'; class UsersAjaxAPI extends AjaxController { diff --git a/include/class.note.php b/include/class.note.php new file mode 100644 index 0000000000000000000000000000000000000000..55cee2c26486ca5a011450a3b34911aa7a82add1 --- /dev/null +++ b/include/class.note.php @@ -0,0 +1,72 @@ +<?php +/********************************************************************* + class.note.php + + Simple note interface for affixing notes to users and organizations + + Peter Rotich <peter@osticket.com> + Jared Hancock <jared@osticket.com> + Copyright (c) 2006-2013 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: +**********************************************************************/ +require_once(INCLUDE_DIR . 'class.orm.php'); + +class QuickNoteModel extends VerySimpleModel { + static $meta = array( + 'table' => NOTE_TABLE, + 'pk' => array('id'), + 'ordering' => array('sort', 'created') + ); +} + +class QuickNote extends QuickNoteModel { + + static $types = array( + 'U' => 'User', + 'O' => 'Organization', + ); + var $_staff; + + function display() { + return Format::display($this->body); + } + + function getStaff() { + if (!isset($this->_staff) && $this->staff_id) { + $this->_staff = Staff::lookup($this->staff_id); + } + return $this->_staff; + } + + function getFormattedTime() { + return Format::db_datetime(strpos($this->updated, '0000-') !== 0 + ? $this->updated : $this->created); + } + + function getExtType() { + return static::$types[$this->ext_id[0]]; + } + + static function forUser($user, $org=false) { + if ($org) + return static::objects()->filter(array('ext_id__in' => + array('U'.$user->get('id'), 'O'.$org->get('id')))); + else + return static::objects()->filter(array('ext_id' => 'U'.$user->get('id'))); + } + + static function forOrganization($org) { + return static::objects()->filter(array('ext_id' => 'O'.$org->get('id'))); + } + + function save($refetch=false) { + if (count($this->dirty)) + $this->updated = new SQLFunction('NOW'); + return parent::save($refetch); + } +} diff --git a/include/staff/org-view.inc.php b/include/staff/org-view.inc.php index 7f6403765f04af8ad4a06076136f7b7a0ae2d319..5f040395f9337fc872798cbd17473f33e0bdc7e6 100644 --- a/include/staff/org-view.inc.php +++ b/include/staff/org-view.inc.php @@ -53,6 +53,8 @@ if(!defined('OSTSCPINC') || !$thisstaff || !is_object($org)) die('Invalid path') class="icon-user"></i> Users</a></li> <li><a id="tickets_tab" href="#tickets"><i class="icon-list-alt"></i> Tickets</a></li> + <li><a id="notes_tab" href="#notes"><i + class="icon-pushpin"></i> Notes</a></li> </ul> <div class="tab_content" id="users"> <?php @@ -65,6 +67,14 @@ include STAFFINC_DIR . 'templates/tickets.tmpl.php'; ?> </div> +<div class="tab_content" id="notes" style="display:none"> +<?php +$notes = QuickNote::forOrganization($org); +$ext_id = 'O'.$org->getId(); +include STAFFINC_DIR . 'templates/notes.tmpl.php'; +?> +</div> + <script type="text/javascript"> $(function() { $(document).on('click', 'a.org-action', function(e) { diff --git a/include/staff/templates/note.tmpl.php b/include/staff/templates/note.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..c0605028b6fbf29db7a7d9ee49a19d60722138bd --- /dev/null +++ b/include/staff/templates/note.tmpl.php @@ -0,0 +1,25 @@ +<div class="quicknote" data-id="<?php echo $note->id; ?>"> + <div class="header"> + <div class="header-left"> + <?php echo $note->getFormattedTime(); ?> + </div> + <div class="header-right"> +<?php + echo $note->getStaff()->getName(); +if ($ext_id && $note->ext_id != $ext_id) { ?> + <span class="label label-info"><?php echo $note->getExtType(); ?></span> +<?php } +if (isset($show_options) && $show_options) { ?> + <div class="options"> + <a href="#" class="action edit-note no-pjax" title="edit"><i class="icon-edit"></i></a> + <a href="#" class="action save-note no-pjax" style="display:none" title="save"><i class="icon-save"></i></a> + <a href="#" class="action cancel-edit no-pjax" style="display:none" title="undo"><i class="icon-undo"></i></a> + <a href="#" class="action delete no-pjax" title="delete"><i class="icon-trash"></i></a> + </div> +<?php } ?> + </div> + </div> + <div class="body editable"> + <?php echo $note->display(); ?> + </div> +</div> diff --git a/include/staff/templates/notes.tmpl.php b/include/staff/templates/notes.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..dca6477151481303aee4325a04e97bd224d5dafb --- /dev/null +++ b/include/staff/templates/notes.tmpl.php @@ -0,0 +1,12 @@ +<div id="quick-notes"> +<?php +$show_options = true; +foreach ($notes as $note) { + include STAFFINC_DIR."templates/note.tmpl.php"; +} ?> +</div> +<div class="quicknote" id="new-note" data-ext-id="<?php echo $ext_id; ?>"> +<div class="body"> + <a href="#"><i class="icon-plus icon-large"></i> Click to create a new note</a> +</div> +</div> diff --git a/include/staff/templates/user.tmpl.php b/include/staff/templates/user.tmpl.php index cacd574b395c60c4e196fcd8c474fb88a98b302f..5bdf31d84788faf6976124e40d4786953de487d9 100644 --- a/include/staff/templates/user.tmpl.php +++ b/include/staff/templates/user.tmpl.php @@ -27,20 +27,68 @@ if ($info['error']) { <div><?php echo $org->getName(); ?></div> <?php } ?> - <table style="margin-top: 1em;"> + +<div class="clear"></div> +<ul class="tabs" style="margin-top:5px"> + <li><a href="#info-tab" class="active" + ><i class="icon-info-sign"></i> Client</a></li> +<?php if ($org) { ?> + <li><a href="#organization-tab" + ><i class="icon-fixed-width icon-building"></i> Organization</a></li> +<?php } + $ext_id = "U".$user->getId(); + if (($notes = QuickNote::forUser($user, $org)->all())) { ?> + <li><a href="#notes-tab" + ><i class="icon-fixed-width icon-pushpin"></i> Notes</a></li> +<?php } ?> +</ul> + +<div class="tab_content" id="info-tab"> + <table class="custom-info"> <?php foreach ($user->getDynamicData() as $entry) { ?> - <tr><td colspan="2" style="border-bottom: 1px dotted black"><strong><?php + <tr><th colspan="2"><strong><?php + echo $entry->getForm()->get('title'); ?></strong></td></tr> +<?php foreach ($entry->getAnswers() as $a) { ?> + <tr><td style="width:30%;"><?php echo Format::htmlchars($a->getField()->get('label')); + ?>:</td> + <td><?php echo $a->display(); ?></td> + </tr> +<?php } +} +?> + </table> +</div> + +<div class="tab_content" id="organization-tab" style="display:none"> + <table class="custom-info"> +<?php foreach ($org->getDynamicData() as $entry) { +?> + <tr><th colspan="2"><strong><?php echo $entry->getForm()->get('title'); ?></strong></td></tr> <?php foreach ($entry->getAnswers() as $a) { ?> - <tr style="vertical-align:top"><td style="width:30%;border-bottom: 1px dotted #ccc"><?php echo Format::htmlchars($a->getField()->get('label')); + <tr><td style="width:30%"><?php echo Format::htmlchars($a->getField()->get('label')); ?>:</td> - <td style="border-bottom: 1px dotted #ccc"><?php echo $a->display(); ?></td> + <td><?php echo $a->display(); ?></td> </tr> <?php } } ?> </table> +</div> + +<div class="tab_content" id="notes-tab" style="display:none"> +<?php $show_options = true; +foreach ($notes as $note) + include STAFFINC_DIR . 'templates/note.tmpl.php'; +?> +<div class="quicknote no-options" id="new-note" data-ext-id="U<?php echo $user->getId(); ?>"> +<div class="body"> + <a href="#"><i class="icon-plus icon-large"></i> Click to create a new note</a> +</div> +</div> +</div> + <div class="clear"></div> <hr> <div class="faded">Last updated <b><?php echo Format::db_datetime($user->getUpdateDate()); ?> </b></div> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index d155e040cfb7446f5c546cedc5a3cdee77b58e55..1849851cc487617910108c8de1ee6f88d1dbe2d9 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -178,13 +178,14 @@ if($ticket->isOverdue()) <ul> <?php if(($open=$user->getNumOpenTickets())) - echo sprintf('<li><a href="tickets.php?a=search&status=open&uid=%s"><i class="icon-folder-open-alt"></i> %d Open Tickets</a></li>', + echo sprintf('<li><a href="tickets.php?a=search&status=open&uid=%s"><i class="icon-folder-open-alt icon-fixed-width"></i> %d Open Tickets</a></li>', $user->getId(), $open); if(($closed=$user->getNumClosedTickets())) - echo sprintf('<li><a href="tickets.php?a=search&status=closed&uid=%d"><i class="icon-folder-close-alt"></i> %d Closed Tickets</a></li>', + echo sprintf('<li><a href="tickets.php?a=search&status=closed&uid=%d"><i class="icon-folder-close-alt icon-fixed-width"></i> %d Closed Tickets</a></li>', $user->getId(), $closed); ?> - <li><a href="tickets.php?a=search&uid=<?php echo $ticket->getOwnerId(); ?>"><i class="icon-double-angle-right"></i> All Tickets</a></li> + <li><a href="tickets.php?a=search&uid=<?php echo $ticket->getOwnerId(); ?>"><i class="icon-double-angle-right icon-fixed-width"></i> All Tickets</a></li> + <li><a href="users.php?id=<?php echo $user->getId(); ?>"><i class="icon-user icon-fixed-width"></i> Manage Client</a></li> </u> </div> <?php diff --git a/include/staff/user-view.inc.php b/include/staff/user-view.inc.php index 0e297150cbfce08f75add4395e6393f50ceeda50..69cb2a55839fe369deb4815d597d789cbe90861e 100644 --- a/include/staff/user-view.inc.php +++ b/include/staff/user-view.inc.php @@ -119,13 +119,23 @@ $org = $user->getOrganization(); <ul class="tabs"> <li><a class="active" id="tickets_tab" href="#tickets"><i class="icon-list-alt"></i> User Tickets</a></li> + <li><a id="notes_tab" href="#notes"><i + class="icon-pushpin"></i> Notes</a></li> </ul> -<div id="tickets"> +<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); +$ext_id = 'U'.$user->getId(); +include STAFFINC_DIR . 'templates/notes.tmpl.php'; +?> +</div> + <div style="display:none;" class="dialog" id="confirm-action"> <h3>Please Confirm</h3> <a class="close" href=""><i class="icon-remove-circle"></i></a> diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js index 0d4c202db9a824108eb46a91daaa2606bb132917..d93a9ee679cb707432559c472e0ee5bbc2360d02 100644 --- a/js/redactor-osticket.js +++ b/js/redactor-osticket.js @@ -206,9 +206,9 @@ $(function() { html = html.replace(/<inline /, '<span ').replace(/<\/inline>/, '</span>'); return html; }, - redact = function(el) { + redact = $.redact = function(el, options) { var el = $(el), - options = { + options = $.extend({ 'air': el.hasClass('no-bar'), 'airButtons': ['formatting', '|', 'bold', 'italic', 'underline', 'deleted', '|', 'unorderedlist', 'orderedlist', 'outdent', 'indent', '|', 'image'], 'buttons': ['html', '|', 'formatting', '|', 'bold', @@ -226,7 +226,7 @@ $(function() { 'tabFocus': false, 'toolbarFixedBox': true, 'focusCallback': function() { this.$box.addClass('no-pjax'); } - }; + }, options||{}); if (el.data('redactor')) return; var reset = $('input[type=reset]', el.closest('form')); if (reset) { diff --git a/scp/ajax.php b/scp/ajax.php index d0830bdf84cf81116154d9caf9f29fd0b1272d28..a445bfeb92977fae99efd069859abb0468971135 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -142,6 +142,12 @@ $dispatcher = patterns('', url_post('^(?P<namespace>[\w.]+)$', 'createDraft'), url_get('^images/browse$', 'getFileList') )), + url('^/note/', patterns('ajax.note.php:NoteAjaxAPI', + url_get('^(?P<id>\d+)$', 'getNote'), + url_post('^(?P<id>\d+)$', 'updateNote'), + url_delete('^(?P<id>\d+)$', 'deleteNote'), + url_post('^attach/(?P<ext_id>\w\d+)$', 'createNote') + )), url_post('^/upgrader', array('ajax.upgrader.php:UpgraderAjaxAPI', 'upgrade')), url('^/help/', patterns('ajax.tips.php:HelpTipAjaxAPI', url_get('^tips/(?P<namespace>[\w_.]+)$', 'getTipsJson'), diff --git a/scp/css/scp.css b/scp/css/scp.css index 3e93fa84b4408b6511ff604f85e3869b1c25df2d..53741572a0cff45c48c33f7c34c4f49bf8da0be3 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -1643,3 +1643,80 @@ tr.disabled th { opacity: 0.6; background: #f5f5f5; } + +.quicknote { + margin: 10px 0; + border: 1px solid rgba(0,0,0,0.2); + border-radius: 4px; +} +.quicknote .header { + position: relative; + display: block; + padding: 10px; + border-bottom: 1px dashed rgba(0,0,0,0.2); +} +.quicknote .header .header-left { + display: inline-block; +} +.quicknote .header .header-right { + display: inline-block; + text-align: right; + right: 1em; + position: absolute; +} +.quicknote .header .options { + display: inline-block; + padding-left: 5px; + margin-left: 10px; + white-space: nowrap; + border-left: 1px solid rgba(0,0,0,0.2); +} +.quicknote .body { + padding: 10px; +} +.quicknote a.action { + padding: 2px 4px; + margin: 1px; + color: black !important; +} +.quicknote a.action:hover { + text-decoration: none; + border: 1px solid #ff9100; + border-radius: 3px; + color: #ff9100 !important; + margin: 0; +} +#new-note { + margin-top: 10px; +} + +.label { + font-size: 11px; + padding: 1px 4px 2px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + font-weight: bold; + line-height: 14px; + color: #ffffff; + vertical-align: baseline; + white-space: nowrap; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #999999; +} +.label-info { + background-color: #3a87ad; +} + +table.custom-info th { + background: transparent; + border: none; + padding-top: 10px; + border-bottom: 1px dotted rgba(0,0,0,0.9); +} +table.custom-info tr { + vertical-align: top; +} +table.custom-info td { + border-bottom: 1px dotted rgba(0,0,0,0.3); +} diff --git a/scp/js/scp.js b/scp/js/scp.js index b93151a2fc317c8b08ddce39e7999e9b84f4b8d4..3712f8904f590d0b74d2434251dc9eb7c52321db 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -662,3 +662,88 @@ $(document).on('click', 'a', function() { $(this).addClass('active'); } }); + +// Quick note interface +$('.quicknote .action.edit-note').live('click.note', function() { + var note = $(this).closest('.quicknote'), + body = note.find('.body'), + T = $('<textarea>').text(body.html()); + if (note.closest('.dialog').length) + T.addClass('no-bar small'); + body.replaceWith(T); + $.redact(T); + $(T).redactor('focus'); + note.find('.action.edit-note').hide(); + note.find('.action.save-note').show(); + note.find('.action.cancel-edit').show(); + return false; +}); +$('.quicknote .action.cancel-edit').live('click.note', function() { + var note = $(this).closest('.quicknote'), + T = note.find('textarea'), + body = $('<div class="body">'); + body.load('ajax.php/note/' + note.data('id'), function() { + try { T.redactor('destroy'); } catch (e) {} + T.replaceWith(body); + note.find('.action.save-note').hide(); + note.find('.action.cancel-edit').hide(); + note.find('.action.edit-note').show(); + }); + return false; +}); +$('.quicknote .action.save-note').live('click.note', function() { + var note = $(this).closest('.quicknote'), + T = note.find('textarea'); + $.post('ajax.php/note/' + note.data('id'), + { note: T.redactor('get') }, + function(html) { + var body = $('<div class="body">').html(html); + try { T.redactor('destroy'); } catch (e) {} + T.replaceWith(body); + note.find('.action.save-note').hide(); + note.find('.action.cancel-edit').hide(); + note.find('.action.edit-note').show(); + }, + 'html' + ); + return false; +}); +$('.quicknote .delete').live('click.note', function() { + var that = $(this), + id = $(this).closest('.quicknote').data('id'); + $.ajax('ajax.php/note/' + id, { + type: 'delete', + success: function() { + that.closest('.quicknote').animate( + {height: 0, opacity: 0}, 'slow', function() { + $(this).remove(); + }); + } + }); + return false; +}); +$('#new-note').live('click', function() { + var note = $(this).closest('.quicknote'), + top = note.parent(), + T = $('<textarea>'), + button = $('<input type="button">').val('Create'); + button.click(function() { + $.post('ajax.php/note/attach/' + note.data('extId'), + { note: T.redactor('get'), no_options: note.hasClass('no-options') }, + function(response) { + $(T).redactor('destroy').replaceWith(note); + $(response).show('highlight').insertBefore(note); + $('.submit', note.parent()).remove(); + }, + 'html' + ); + }); + if (note.closest('.dialog').length) + T.addClass('no-bar small'); + note.replaceWith(T); + $('<p>').addClass('submit').css('text-align', 'center') + .append(button).appendTo(T.parent()); + $.redact(T); + $(T).redactor('focus'); + return false; +}); diff --git a/scp/orgs.php b/scp/orgs.php index 0ecb8d1892cbed3f27610139a5a6fb0d4e3a53af..024deec5dbcac33a0b1f383d8dcabc444bf7c647 100644 --- a/scp/orgs.php +++ b/scp/orgs.php @@ -13,6 +13,8 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ require('staff.inc.php'); +require_once INCLUDE_DIR . 'class.note.php'; + $org = null; if ($_REQUEST['id']) $org = Organization::lookup($_REQUEST['id']); diff --git a/scp/users.php b/scp/users.php index 3020a91f19fceb60517d817382d28a1cdfb1e2e2..4130be6ca86d5e34ce621a0f4b524156ef9ab45c 100644 --- a/scp/users.php +++ b/scp/users.php @@ -13,6 +13,9 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ require('staff.inc.php'); + +require_once INCLUDE_DIR.'class.note.php'; + $user = null; if ($_REQUEST['id'] && !($user=User::lookup($_REQUEST['id']))) $errors['err'] = 'Unknown or invalid user ID.';