From f0dd07fa289db0506836363fc5fb0896378bd908 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Fri, 7 Mar 2014 16:15:25 +0100 Subject: [PATCH] Add resource searching/booking capabilities to the calendar module --- plugins/calendar/calendar.php | 55 ++- plugins/calendar/calendar_base.js | 2 +- plugins/calendar/calendar_ui.js | 320 ++++++++++++++++-- plugins/calendar/drivers/calendar_driver.php | 32 ++ .../calendar/drivers/kolab/kolab_calendar.php | 14 +- .../calendar/drivers/kolab/kolab_driver.php | 104 ++++++ plugins/calendar/lib/calendar_ui.php | 85 ++++- plugins/calendar/localization/en_US.inc | 10 + .../skins/classic/templates/eventedit.html | 16 +- plugins/calendar/skins/larry/calendar.css | 120 ++++++- .../skins/larry/templates/calendar.html | 42 +++ .../skins/larry/templates/eventedit.html | 20 +- 12 files changed, 758 insertions(+), 62 deletions(-) diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 4e8995ed..152cc539 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -141,6 +141,9 @@ class calendar extends rcube_plugin $this->register_action('mailtoevent', array($this, 'mail_message2event')); $this->register_action('inlineui', array($this, 'get_inline_ui')); $this->register_action('check-recent', array($this, 'check_recent')); + $this->register_action('resources-list', array($this, 'resources_list')); + $this->register_action('resources-owner', array($this, 'resources_owner')); + $this->register_action('resources-autocomplete', array($this, 'resources_autocomplete')); $this->add_hook('refresh', array($this, 'refresh')); // remove undo information... @@ -287,13 +290,14 @@ class calendar extends rcube_plugin $this->ui->addJS(); $this->ui->init_templates(); - $this->rc->output->add_label('lowest','low','normal','high','highest','delete','cancel','uploading','noemailwarning'); + $this->rc->output->add_label('lowest','low','normal','high','highest','delete','cancel','uploading','noemailwarning','close'); // initialize attendees autocompletion rcube_autocomplete_init(); $this->rc->output->set_env('timezone', $this->timezone->getName()); $this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false); + $this->rc->output->set_env('resources', (bool)$this->driver->resources); $this->rc->output->set_env('mscolors', $this->driver->get_color_values()); $this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list'))); @@ -1930,6 +1934,55 @@ class calendar extends rcube_plugin } + /**** Resource management functions ****/ + + public function resources_autocomplete() + { + $search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true); + $sid = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); + $maxnum = (int)$this->rc->config->get('autocomplete_max', 15); + $results = array(); + + foreach ($this->driver->load_resources($search, $maxnum) as $rec) { + $results[] = array( + 'name' => $rec['name'], + 'email' => $rec['email'], + 'type' => $rec['_type'], + ); + } + + $this->rc->output->command('ksearch_query_results', $results, $search, $sid); + $this->rc->output->send(); + } + + /** + * Handler for load-requests for resource data + */ + function resources_list() + { + $data = array(); + foreach ($this->driver->load_resources() as $rec) { + $rec['dn'] = rcube_ldap::dn_decode($rec['ID']); + $data[] = $rec; + } + + $this->rc->output->command('plugin.resource_data', $data); + $this->rc->output->send(); + } + + /** + * Handler for requests loading resource owner information + */ + function resources_owner() + { + $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); + $data = $this->driver->get_resource_owner($id); + + $this->rc->output->command('plugin.resource_owner', $data); + $this->rc->output->send(); + } + + /**** Event invitation plugin hooks ****/ /** diff --git a/plugins/calendar/calendar_base.js b/plugins/calendar/calendar_base.js index 33fe9e4d..e7039597 100644 --- a/plugins/calendar/calendar_base.js +++ b/plugins/calendar/calendar_base.js @@ -48,7 +48,7 @@ function rcube_calendar(settings) ).then(function() { // disable attendees feature (autocompletion and stuff is not initialized) for (var c in rcmail.env.calendars) - rcmail.env.calendars[c].attendees = false; + rcmail.env.calendars[c].attendees = rcmail.env.calendars[c].resources = false; me.ui_loaded = true; me.ui = new rcube_calendar_ui(me.settings); diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index ff6342b5..61e02a05 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -47,6 +47,10 @@ function rcube_calendar_ui(settings) var event_defaults = { free_busy:'busy', alarms:'' }; var event_attendees = []; var attendees_list; + var resources_list; + var resources_treelist; + var resources_data = {}; + var resources_index = []; var freebusy_ui = { workinhoursonly:false, needsupdate:false }; var freebusy_data = {}; var current_view = null; @@ -103,6 +107,11 @@ function rcube_calendar_ui(settings) return result; }; + // Change the first charcter to uppercase + var ucfirst = function(str) + { + return str.charAt(0).toUpperCase() + str.substr(1); + }; // clone the given date object and optionally adjust time var clone_date = function(date, adjust) @@ -332,6 +341,13 @@ function rcube_calendar_ui(settings) // list event attendees if (calendar.attendees && event.attendees) { + // sort resources to the end + event.attendees.sort(function(a,b) { + var j = a.cutype == 'RESOURCE' ? 1 : 0, + k = b.cutype == 'RESOURCE' ? 1 : 0; + return (j - k); + }); + var data, dispname, organizer = false, rsvp = false, line, morelink, html = '',overflow = ''; for (var j=0; j < event.attendees.length; j++) { data = event.attendees[j]; @@ -340,7 +356,7 @@ function rcube_calendar_ui(settings) dispname = '' + dispname + ''; if (data.role == 'ORGANIZER') organizer = true; - else if ((data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE') && settings.identity.emails.indexOf(';'+data.email) >= 0) + else if ((data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp) && settings.identity.emails.indexOf(';'+data.email) >= 0) rsvp = data.status.toLowerCase(); } @@ -567,6 +583,7 @@ function rcube_calendar_ui(settings) allow_invitations = organizer || (calendar.owner && calendar.owner == 'anonymous') || settings.invite_shared; event_attendees = []; attendees_list = $('#edit-attendees-table > tbody').html(''); + resources_list = $('#edit-resources-table > tbody').html(''); $('#edit-attendees-notify')[(notify.checked && allow_invitations ? 'show' : 'hide')](); $('#edit-localchanges-warning')[(has_attendees(event) && !(allow_invitations || (calendar.owner && is_organizer(event, calendar.owner))) ? 'show' : 'hide')](); @@ -744,6 +761,7 @@ function rcube_calendar_ui(settings) // show/hide tabs according to calendar's feature support $('#edit-tab-attendees')[(calendar.attendees?'show':'hide')](); + $('#edit-tab-resources')[(calendar.resources?'show':'hide')](); $('#edit-tab-attachments')[(calendar.attachments?'show':'hide')](); // activate the first tab @@ -1405,9 +1423,10 @@ function rcube_calendar_ui(settings) $('#edit-startdate').data('duration', Math.round((me.selected_event.end.getTime() - me.selected_event.start.getTime()) / 1000)); } }; - + + // add the given list of participants - var add_attendees = function(names) + var add_attendees = function(names, params) { names = explode_quoted_string(names.replace(/,\s*$/, ''), ','); @@ -1430,9 +1449,8 @@ function rcube_calendar_ui(settings) email = RegExp.$1; name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, ''); } - if (email) { - add_attendee({ email:email, name:name, role:'REQ-PARTICIPANT', status:'NEEDS-ACTION' }); + add_attendee($.extend({ email:email, name:name }, params)); success = true; } else { @@ -1442,16 +1460,21 @@ function rcube_calendar_ui(settings) return success; }; - + // add the given attendee to the list var add_attendee = function(data, readonly) { + if (!me.selected_event) + return false; + // check for dupes... var exists = false; $.each(event_attendees, function(i, v){ exists |= (v.email == data.email); }); if (exists) return false; + var calendar = me.selected_event && me.calendars[me.selected_event.calendar] ? me.calendars[me.selected_event.calendar] : me.calendars[me.selected_calendar]; + var dispname = Q(data.name || data.email); if (data.email) dispname = '' + dispname + ''; @@ -1464,8 +1487,10 @@ function rcube_calendar_ui(settings) opts['REQ-PARTICIPANT'] = rcmail.gettext('calendar.rolerequired'); opts['OPT-PARTICIPANT'] = rcmail.gettext('calendar.roleoptional'); opts['NON-PARTICIPANT'] = rcmail.gettext('calendar.rolenonparticipant'); - opts['CHAIR'] = rcmail.gettext('calendar.rolechair'); - + + if (data.cutype != 'RESOURCE') + opts['CHAIR'] = rcmail.gettext('calendar.rolechair'); + if (organizer && !readonly) dispname = rcmail.env['identities-selector']; @@ -1483,15 +1508,16 @@ function rcube_calendar_ui(settings) var html = '' + select + '' + '' + dispname + '' + - '' + + '' + '' + Q(data.status || '') + '' + '' + (organizer || readonly ? '' : dellink) + ''; - + + var table = calendar.resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list; var tr = $('') .addClass(String(data.role).toLowerCase()) .html(html) - .appendTo(attendees_list); - + .appendTo(table); + tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; }); tr.find('a.mailtolink').click(function(e) { rcmail.redirect(rcmail.url('mail/compose', { _to:this.href.substr(7) })); return false; }); @@ -1505,16 +1531,17 @@ function rcube_calendar_ui(settings) } event_attendees.push(data); + return true; }; // iterate over all attendees and update their free-busy status display var update_freebusy_status = function(event) { - var icons = attendees_list.find('img.availabilityicon'); - for (var i=0; i < event_attendees.length; i++) { - if (icons.get(i) && event_attendees[i].email) - check_freebusy_status(icons.get(i), event_attendees[i].email, event); - } + attendees_list.find('img.availabilityicon').each(function(i,v) { + var email, icon = $(this); + if (email = icon.attr('data-email')) + check_freebusy_status(icon, email, event); + }); freebusy_ui.needsupdate = false; }; @@ -1550,7 +1577,217 @@ function rcube_calendar_ui(settings) $(elem).closest('tr').remove(); event_attendees = $.grep(event_attendees, function(data){ return (data.name != id && data.email != id) }); }; - + + // open a dialog to display detailed free-busy information and to find free slots + var event_resources_dialog = function() + { + var $dialog = $('#eventresourcesdialog'), + event = me.selected_event; + + if ($dialog.is(':ui-dialog')) + $dialog.dialog('close'); + + // dialog buttons + var buttons = {}; + + buttons[rcmail.gettext('addresource', 'calendar')] = function() { + rcmail.command('add-resource'); + }; + + buttons[rcmail.gettext('close')] = function() { + $dialog.dialog("close"); + }; + + // open jquery UI dialog + $dialog.dialog({ + modal: true, + resizable: true, + closeOnEscape: true, + title: rcmail.gettext('findresources', 'calendar'), + close: function() { + $dialog.dialog('destroy').hide(); + }, + buttons: buttons, + width: Math.min(1000, $(window).width() - 50), + height: 500 + }).show(); + + // initialize the treelist widget + if (!resources_treelist) { + resources_treelist = new rcube_treelist_widget(rcmail.gui_objects.resourceslist, { + id_prefix: 'rcres', + id_encode: rcmail.html_identifier_encode, + id_decode: rcmail.html_identifier_decode, + selectable: true + }); + resources_treelist.addEventListener('select', function(node) { + if (resources_data[node.id]) { + resource_showinfo(resources_data[node.id]); + rcmail.enable_command('add-resource', me.selected_event ? true : false); + } + else { + rcmail.enable_command('add-resource', false); + $(rcmail.gui_objects.resourceinfo).hide(); + $(rcmail.gui_objects.resourceownerinfo).hide(); + } + }); + + // fetch (all) resource data from server + me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock); + rcmail.http_request('resources-list', {}, me.loading_lock); + } + else { + resources_treelist.select('__none__'); + } + + // register button + $('.ui-dialog-buttonset .ui-button', $dialog.parent()).first().addClass('mainaction').attr('id', 'rcmbtncalresadd'); + rcmail.register_button('add-resource', 'rcmbtncalresadd', 'input'); + }; + + // render the resource details UI box + var resource_showinfo = function(resource) + { + // inline function to render a resource attribute + function render_attrib(value) { + if (typeof value == 'boolean') { + return value ? rcmail.get_label('yes') : rcmail.get_label('no'); + } + + return value; + } + + if (rcmail.gui_objects.resourceinfo) { + var tr, table = $(rcmail.gui_objects.resourceinfo).show().find('tbody').html(''), + attribs = $.extend({ name:resource.name }, resource.attributes||{}) + attribs.description = resource.description; + + for (var k in attribs) { + if (typeof attribs[k] == 'undefined') + continue; + table.append($('').addClass(k) + .append('' + Q(ucfirst(rcmail.get_label(k, 'calendar'))) + '') + .append('' + text2html(render_attrib(attribs[k])) + '') + ); + } + + $(rcmail.gui_objects.resourceownerinfo).hide(); + + if (resource.owner) { + // fetch owner data from server + me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock); + rcmail.http_request('resources-owner', { _id: resource.owner }, me.loading_lock); + } + } + }; + + // callback from server for resource listing + var resource_data_load = function(data) + { + data.sort(function(a,b) { + var j = a._type == 'collection' ? 1 : 0, + k = b._type == 'collection' ? 1 : 0; + return k != j ? (j - k) : (a.name < b.name ? 1 : 0); + }); + + // assign parent-relations + $.each(data, function(i, rec) { + resources_data[rec.dn] = rec; + resources_index.push(rec.dn); + + if (rec.members) { + $.each(rec.members, function(j, m){ + resources_data[m].parent_id = rec.dn; + }); + } + }); + + resources_index.reverse(); + resource_render_list(resources_index); + rcmail.set_busy(false, null, me.loading_lock); + }; + + // renders the given list of resource records into the treelist + var resource_render_list = function(index) { + var rec, link; + + resources_treelist.reset(); + + $.each(index, function(i, dn) { + if (rec = resources_data[dn]) { + link = $('').attr('href', '#') + .attr('rel', rec.dn) + .html(Q(rec.name)); + + resources_treelist.insert({ id:rec.dn, html:link, classes:[rec._type], collapsed:true }, rec.parent_id, false); + } + }); + }; + + // callback from server for owner information display + var resource_owner_load = function(data) + { + if (data) { + var table = $(rcmail.gui_objects.resourceownerinfo).find('tbody').html(''); + + for (var k in data) { + if (k == 'event') + continue; + + table.append($('').addClass(k) + .append('' + Q(ucfirst(rcmail.get_label('owner'+k, 'calendar'))) + '') + .append('' + text2html(data[k]) + '') + ); + } + + table.parent().show(); + } + } + + // quick-filter the loaded resource data + var resource_search = function() + { + var dataset, rec, q = $('#resourcesearchbox').val().toLowerCase(); + if (q.length && resources_data) { + dataset = []; + + // search by iterating over all resource records + for (var dn in resources_data) { + rec = resources_data[dn]; + if (String(rec.name).toLowerCase().indexOf(q) >= 0) { + dataset.push(rec.dn); + } + } + + resource_render_list(dataset); + + // select single match + if (dataset.length == 1) { + resources_treelist.select(dataset[0]); + } + } + else { + $('#resourcesearchbox').val(''); + } + }; + + // + var reset_resource_search = function() + { + $('#resourcesearchbox').val('').focus(); + resource_render_list(resources_index); + }; + + // + var add_resource2event = function() + { + var resource = resources_data[resources_treelist.get_selection()]; + if (resource) { + if (add_attendee($.extend({ role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' }, resource))) + rcmail.display_message(rcmail.get_label('resourceadded', 'calendar'), 'confirmation'); + } + } + // when the user accepts or declines an event invitation var event_rsvp = function(response) { @@ -2073,7 +2310,7 @@ function rcube_calendar_ui(settings) if (range == 'custom') start = date2unixtime(parse_datetime('00:00', $('#event-export-startdate').val())); else if (range > 0) - start = 'today -' + range + '^months'; + start = 'today -' + range + ' months'; rcmail.goto_url('export_events', { source:source, start:start, attachments:attachmt?1:0 }); } @@ -2211,6 +2448,12 @@ function rcube_calendar_ui(settings) window.history.replaceState({}, document.title, rcmail.url('', query).replace('&_action=', '')); }; + this.resource_search = resource_search; + this.reset_resource_search = reset_resource_search; + this.add_resource2event = add_resource2event; + this.resource_data_load = resource_data_load; + this.resource_owner_load = resource_owner_load; + /*** event searching ***/ @@ -2759,8 +3002,9 @@ function rcube_calendar_ui(settings) // init event dialog $('#eventtabs').tabs({ show: function(event, ui) { - if (ui.panel.id == 'event-tab-3') { - $('#edit-attendee-name').select(); + if (ui.panel.id == 'event-panel-attendees' || ui.panel.id == 'event-panel-resources') { + var tab = ui.panel.id == 'event-panel-resources' ? 'resource' : 'attendee'; + $('#edit-'+tab+'-name').select(); // update free-busy status if needed if (freebusy_ui.needsupdate && me.selected_event) update_freebusy_status(me.selected_event); @@ -2827,16 +3071,39 @@ function rcube_calendar_ui(settings) }; } rcmail.init_address_input_events($('#edit-attendee-name'), ac_props); - rcmail.addEventListener('autocomplete_insert', function(e){ $('#edit-attendee-add').click(); }); + rcmail.addEventListener('autocomplete_insert', function(e){ + if (e.field.name == 'participant') { + $('#edit-attendee-add').click(); + } + else if (e.field.name == 'resource' && e.data && e.data.email) { + add_attendee($.extend(e.data, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' })); + e.field.value = ''; + } + }); $('#edit-attendee-add').click(function(){ var input = $('#edit-attendee-name'); rcmail.ksearch_blur(); - if (add_attendees(input.val())) { + if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'INDIVIDUAL' })) { input.val(''); } }); + rcmail.init_address_input_events($('#edit-resource-name'), { action:'calendar/resources-autocomplete' }); + + $('#edit-resource-add').click(function(){ + var input = $('#edit-resource-name'); + rcmail.ksearch_blur(); + if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' })) { + input.val(''); + } + }); + + $('#edit-resource-find').click(function(){ + event_resources_dialog(); + return false; + }); + // keep these two checkboxes in sync $('#edit-attendees-donotify, #edit-attendees-invite').click(function(){ $('#edit-attendees-donotify, #edit-attendees-invite').prop('checked', this.checked); @@ -2919,6 +3186,11 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { rcmail.register_command('search', function(){ cal.quicksearch(); }, true); rcmail.register_command('reset-search', function(){ cal.reset_quicksearch(); }, true); + // resource invitation dialog + rcmail.register_command('search-resource', function(){ cal.resource_search(); }, true); + rcmail.register_command('reset-resource-search', function(){ cal.reset_resource_search(); }, true); + rcmail.register_command('add-resource', function(){ cal.add_resource2event(); }, false); + // register callback commands rcmail.addEventListener('plugin.destroy_source', function(p){ cal.calendar_destroy_source(p.id); }); rcmail.addEventListener('plugin.unlock_saving', function(p){ cal.unlock_saving(); }); @@ -2926,6 +3198,8 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { rcmail.addEventListener('plugin.import_success', function(p){ cal.import_success(p); }); rcmail.addEventListener('plugin.import_error', function(p){ cal.import_error(p); }); rcmail.addEventListener('plugin.reload_view', function(p){ cal.reload_view(p); }); + rcmail.addEventListener('plugin.resource_data', function(p){ cal.resource_data_load(p); }); + rcmail.addEventListener('plugin.resource_owner', function(p){ cal.resource_owner_load(p); }); rcmail.addEventListener('requestrefresh', function(q){ return cal.before_refresh(q); }); // let's go diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php index fb29740d..3d8d083a 100644 --- a/plugins/calendar/drivers/calendar_driver.php +++ b/plugins/calendar/drivers/calendar_driver.php @@ -86,6 +86,7 @@ abstract class calendar_driver // features supported by backend public $alarms = false; public $attendees = false; + public $resources = false; public $freebusy = false; public $attachments = false; public $undelete = false; // event undelete action @@ -548,4 +549,35 @@ abstract class calendar_driver return true; } + + /** + * Fetch resource objects to be displayed for booking + * + * @param string Search query (optional) + * @return array List of resource records available for booking + */ + public function load_resources($query = null) + { + return array(); + } + + /** + * Return properties of a single resource + * + * @param mixed UID string + * @return array Resource object as hash array + */ + public function get_resource($uid) + { + return null; + } + + /** + * + */ + public function get_resource_owner($id) + { + return null; + } + } diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php index 49f8fa7d..e93d14f3 100644 --- a/plugins/calendar/drivers/kolab/kolab_calendar.php +++ b/plugins/calendar/drivers/kolab/kolab_calendar.php @@ -218,8 +218,18 @@ class kolab_calendar public function list_events($start, $end, $search = null, $virtual = 1, $query = array()) { // convert to DateTime for comparisons - $start = new DateTime('@'.$start); - $end = new DateTime('@'.$end); + try { + $start = new DateTime('@'.$start); + } + catch (Exception $e) { + $start = new DateTime('@0'); + } + try { + $end = new DateTime('@'.$end); + } + catch (Exception $e) { + $end = new DateTime('today +10 years'); + } // query Kolab storage $query[] = array('dtstart', '<=', $end); diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php index 7b41a31e..0b23f642 100644 --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -60,6 +60,10 @@ class kolab_driver extends calendar_driver $this->alarm_types = array('DISPLAY'); $this->alarm_absolute = false; } + + if ($this->rc->config->get('calendar_resources_directory')) { + $this->resources = true; + } } @@ -1283,4 +1287,104 @@ class kolab_driver extends calendar_driver 'FFDEAD'); } + + private function resurces_ldap() + { + if (!isset($this->resources_dir)) { + $this->resources_dir = new rcube_ldap($this->rc->config->get('calendar_resources_directory'), true); + } + + return $this->resources_dir->ready ? $this->resources_dir : null; + } + + + /** + * Fetch resource objects to be displayed for booking + * + * @param string Search query (optional) + * @return array List of resource records available for booking + */ + public function load_resources($query = null, $num = 5000) + { + if (!($ldap = $this->resurces_ldap())) { + return array(); + } + + // TODO: apply paging + $ldap->set_pagesize($num); + + if (isset($query)) { + $results = $ldap->search('*', $query, 0, true, true); + } + else { + $results = $ldap->list_records(); + } + + if ($results instanceof ArrayAccess) { + foreach ($results as $i => $rec) { + $results[$i] = $this->decode_resource($rec); + } + } + + return $results; + } + + /** + * Return properties of a single resource + * + * @param mixed UID string + * @return array Resource object as hash array + */ + public function get_resource($uid) + { + $rec = null; + + if ($ldap = $this->resurces_ldap()) { + $rec = $ldap->get_record($uid); + + if (!empty($rec)) { + $rec = $this->decode_resource($rec); + } + } + + return $rec; + } + + /** + * + */ + public function get_resource_owner($dn) + { + $owner = null; + + if ($ldap = $this->resurces_ldap()) { + $owner = $ldap->get_record(rcube_ldap::dn_encode($dn), true); + unset($owner['_raw_attrib'], $owner['_type'], $owner['ID']); + } + + return $owner; + } + + /** + * Extract JSON-serialized attributes + */ + private function decode_resource($rec) + { + if (is_array($rec['attributes']) && $rec['attributes'][0]) { + $attributes = array(); + + foreach ($rec['attributes'] as $sattr) { + $attr = @json_decode($sattr, true); + $attributes += $attr; + } + + $rec['attributes'] = $attributes; + } + + // remove unused cruft + unset($rec['_raw_attrib']); + + return $rec; + } + } diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php index 009e6c72..710898f6 100644 --- a/plugins/calendar/lib/calendar_ui.php +++ b/plugins/calendar/lib/calendar_ui.php @@ -84,6 +84,10 @@ class calendar_ui $this->cal->register_handler('plugin.filedroparea', array($this, 'file_drop_area')); $this->cal->register_handler('plugin.attendees_list', array($this, 'attendees_list')); $this->cal->register_handler('plugin.attendees_form', array($this, 'attendees_form')); + $this->cal->register_handler('plugin.resources_form', array($this, 'resources_form')); + $this->cal->register_handler('plugin.resources_list', array($this, 'resources_list')); + $this->cal->register_handler('plugin.resources_searchform', array($this, 'resources_search_form')); + $this->cal->register_handler('plugin.resource_info', array($this, 'resource_info')); $this->cal->register_handler('plugin.attendees_freebusy_table', array($this, 'attendees_freebusy_table')); $this->cal->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify')); $this->cal->register_handler('plugin.edit_recurring_warning', array($this, 'recurring_event_warning')); @@ -112,6 +116,7 @@ class calendar_ui $this->cal->include_script('calendar_ui.js'); $this->cal->include_script('lib/js/fullcalendar.js'); $this->cal->include_script('lib/js/jquery.miniColors.min.js'); + $this->rc->output->include_script('treelist.js'); } /** @@ -191,6 +196,7 @@ class calendar_ui unset($prop['user_id']); $prop['alarms'] = $this->cal->driver->alarms; $prop['attendees'] = $this->cal->driver->attendees; + $prop['resources'] = $this->cal->driver->resources; $prop['freebusy'] = $this->cal->driver->freebusy; $prop['attachments'] = $this->cal->driver->attachments; $prop['undelete'] = $this->cal->driver->undelete; @@ -732,7 +738,7 @@ class calendar_ui { $table = new html_table(array('cols' => 5, 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable')); $table->add_header('role', $this->cal->gettext('role')); - $table->add_header('name', $this->cal->gettext('attendee')); + $table->add_header('name', $this->cal->gettext($attrib['coltitle'] ?: 'attendee')); $table->add_header('availability', $this->cal->gettext('availability')); $table->add_header('confirmstate', $this->cal->gettext('confirmstate')); $table->add_header('options', ''); @@ -755,7 +761,82 @@ class calendar_ui html::p('attendees-invitebox', html::label(null, $checkbox->show(1) . $this->cal->gettext('sendinvitations'))) ); } - + + /** + * + */ + function resources_form($attrib = array()) + { + $input = new html_inputfield(array('name' => 'resource', 'id' => 'edit-resource-name', 'size' => 30)); + + return html::div($attrib, + html::div(null, $input->show() . " " . + html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-resource-add', 'value' => $this->cal->gettext('addresource'))) . " " . + html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-resource-find', 'value' => $this->cal->gettext('findresources').'...'))) + ); + } + + /** + * + */ + function resources_list($attrib = array()) + { + $attrib += array('id' => 'calendar-resources-list'); + + $this->rc->output->add_gui_object('resourceslist', $attrib['id']); + + return html::tag('ul', $attrib, '', html::$common_attrib); + } + + /** + * + */ + public function resource_info($attrib = array()) + { + $attrib += array('id' => 'calendar-resources-info'); + + $this->rc->output->add_gui_object('resourceinfo', $attrib['id']); + $this->rc->output->add_gui_object('resourceownerinfo', $attrib['id'] . '-owner'); + + $table_attrib = array('id','class','style','width','summary','cellpadding','cellspacing','border'); + + return html::tag('table', $attrib, + html::tag('tbody', null, ''), $table_attrib) . + + html::tag('table', array('id' => $attrib['id'] . '-owner', 'style' => 'display:none') + $attrib, + html::tag('thead', null, + html::tag('tr', null, + html::tag('td', array('colspan' => 2), Q($this->cal->gettext('resourceowner'))) + ) + ) . + html::tag('tbody', null, ''), + $table_attrib); + } + + /** + * GUI object 'searchform' for the resource finder dialog + * + * @param array Named parameters + * @return string HTML code for the gui object + */ + function resources_search_form($attrib) + { + $attrib += array('command' => 'search-resource', 'id' => 'rcmcalresqsearchbox', 'autocomplete' => 'off'); + $attrib['name'] = '_q'; + + $input_q = new html_inputfield($attrib); + $out = $input_q->show(); + + // add form tag around text field + $out = $this->rc->output->form_tag(array( + 'name' => "rcmcalresoursqsearchform", + 'onsubmit' => rcmail_output::JS_OBJECT_NAME . ".command('" . $attrib['command'] . "'); return false", + 'style' => "display:inline"), + $out); + + return $out; + } + /** * */ diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc index c99199e2..8d68d725 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -140,6 +140,15 @@ $labels['eventupdatemailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$atten $labels['eventcancelsubject'] = '"$title" has been canceled'; $labels['eventcancelmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nThe event has been cancelled by \$organizer.\n\nPlease find attached an iCalendar file with the updated event details."; +// resources +$labels['resource'] = 'Resource'; +$labels['addresource'] = 'Book resource'; +$labels['findresources'] = 'Find resources'; +$labels['resourcedetails'] = 'Details'; +$labels['resourceavailability'] = 'Availability'; +$labels['resourceowner'] = 'Owner'; +$labels['resourceadded'] = 'The resource was added to your event'; + // invitation handling $labels['itipinvitation'] = 'Invitation to'; $labels['itipupdate'] = 'Update of'; @@ -171,6 +180,7 @@ $labels['saveincalendar'] = 'save in'; $labels['tabsummary'] = 'Summary'; $labels['tabrecurrence'] = 'Recurrence'; $labels['tabattendees'] = 'Participants'; +$labels['tabresources'] = 'Resources'; $labels['tabattachments'] = 'Attachments'; $labels['tabsharing'] = 'Sharing'; diff --git a/plugins/calendar/skins/classic/templates/eventedit.html b/plugins/calendar/skins/classic/templates/eventedit.html index 6e1c2b36..1bc1a123 100644 --- a/plugins/calendar/skins/classic/templates/eventedit.html +++ b/plugins/calendar/skins/classic/templates/eventedit.html @@ -1,13 +1,13 @@
    -
  • -
  • -
  • -
  • +
  • +
  • +
  • +
-
+

@@ -65,7 +65,7 @@
-
+
@@ -86,13 +86,13 @@
-
+
-
+
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css index b0120d17..7efe8684 100644 --- a/plugins/calendar/skins/larry/calendar.css +++ b/plugins/calendar/skins/larry/calendar.css @@ -484,7 +484,7 @@ a.miniColors-trigger { .calendarmain .fc-view-table td.fc-list-header, #attendees-freebusy-table h3.boxtitle, #schedule-freebusy-times thead th, -#edit-attendees-table thead td +.edit-attendees-table thead td { color: #69939e; font-size: 11px; @@ -658,34 +658,34 @@ td.topalign { padding: 0.5em; } -#edit-attendees-table { +.edit-attendees-table { width: 100%; margin-top: 0.5em; } -#edit-attendees-table td.role { +.edit-attendees-table td.role { width: 9em; } -#edit-attendees-table td.availability, -#edit-attendees-table td.confirmstate { +.edit-attendees-table td.availability, +.edit-attendees-table td.confirmstate { width: 4em; } -#edit-attendees-table td.options { +.edit-attendees-table td.options { width: 3em; text-align: right; padding-right: 4px; } -#edit-attendees-table td.name { +.edit-attendees-table td.name { width: auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -#edit-attendees-table a.deletelink { +.edit-attendees-table a.deletelink { display: block; width: 17px; height: 17px; @@ -694,18 +694,20 @@ td.topalign { text-indent: 1000px; } -#edit-attendees-form { +#edit-attendees-form, +#edit-resources-form { position: relative; margin-top: 1em; } -#edit-attendees-form #edit-attendee-schedule { +#edit-attendees-form #edit-attendee-schedule, +#edit-resources-form #edit-resource-find { position: absolute; top: 0; right: 0; } -#edit-attendees-table select.edit-attendee-role { +.edit-attendees-table select.edit-attendee-role { border: 0; padding: 2px; background: white; @@ -778,34 +780,34 @@ td.topalign { vertical-align: middle; } -#edit-attendees-table tbody td.confirmstate { +.edit-attendees-table tbody td.confirmstate { overflow: hidden; white-space: nowrap; text-indent: -2000%; } -#edit-attendees-table td.confirmstate span { +.edit-attendees-table td.confirmstate span { display: block; width: 20px; background: url(images/attendee-status.gif) 5px 0 no-repeat; } -#edit-attendees-table td.confirmstate span.needs-action { +.edit-attendees-table td.confirmstate span.needs-action { } -#edit-attendees-table td.confirmstate span.accepted { +.edit-attendees-table td.confirmstate span.accepted { background-position: 5px -20px; } -#edit-attendees-table td.confirmstate span.declined { +.edit-attendees-table td.confirmstate span.declined { background-position: 5px -40px; } -#edit-attendees-table td.confirmstate span.tentative { +.edit-attendees-table td.confirmstate span.tentative { background-position: 5px -60px; } -#edit-attendees-table td.confirmstate span.delegated { +.edit-attendees-table td.confirmstate span.delegated { background-position: 5px -160px; } @@ -1042,6 +1044,88 @@ a.dropdown-link:after { padding: 0.5em 1em; } +#resource-selection { + position: absolute; + top: 0; + left: 8px; + right: 0; + bottom: 0; +} + +#resource-selection .scroller { + top: 34px; +} + +#resource-dialog-left { + position: absolute; + top: 10px; + left: 0; + width: 380px; + bottom: 10px; +} + +#resource-dialog-right { + position: absolute; + top: 10px; + left: 392px; + right: 8px; + bottom: 10px; +} + +#resource-info { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 56%; +} + +#resource-info table { + margin: 8px; + width: 97%; +} + +#resource-info thead td { + background: none; + font-weight: bold; + font-size: 14px; +} + +#resource-availability { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 40%; +} + +#resourcequicksearch { + padding: 4px; + background: #c7e3ef; +} + +#resourcesearchbox { + width: 100%; + height: 26px; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +#resourcequicksearch .iconbutton.searchoptions { + position: absolute; + top: 5px; + left: 6px; + width: 16px; +} + +.searchbox .iconbutton.reset { + position: absolute; + top: 4px; + right: 1px; +} + + + /* fullcalendar style overrides */ .rcube-fc-content { diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html index 453d59a6..574cded2 100644 --- a/plugins/calendar/skins/larry/templates/calendar.html +++ b/plugins/calendar/skins/larry/templates/calendar.html @@ -113,6 +113,45 @@ +
+
+
+
+ +
+
+ +
+ +
+
+ +
+
+

+
+ +
+
+ +
+

+
+
+
+
+
@@ -205,6 +244,9 @@ $(document).ready(function(e){ }) .data('offset', $('#calendarsidebartoggle').position().left) .data('sidebarwidth', $('#calendarsidebar').width() + $('#calendarsidebar').position().left); + + new rcube_splitter({ id:'calresourceviewsplitter', p1:'#resource-dialog-left', p2:'#resource-dialog-right', + orientation:'v', relative:true, start:380, min:220, size:10, offset:-3 }).init(); }); diff --git a/plugins/calendar/skins/larry/templates/eventedit.html b/plugins/calendar/skins/larry/templates/eventedit.html index 0ae2b774..9f2a3741 100644 --- a/plugins/calendar/skins/larry/templates/eventedit.html +++ b/plugins/calendar/skins/larry/templates/eventedit.html @@ -1,10 +1,10 @@
    -
  • +
-
+

@@ -62,7 +62,7 @@
-
+
@@ -83,20 +83,26 @@
-
- +
+
+ +
+ + + +
-
+
- +