Add resource searching/booking capabilities to the calendar module

This commit is contained in:
Thomas Bruederli 2014-03-07 16:15:25 +01:00
parent e56bd6be79
commit f0dd07fa28
12 changed files with 758 additions and 62 deletions

View file

@ -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 ****/
/**

View file

@ -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);

View file

@ -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 = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink">' + dispname + '</a>';
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 = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink">' + dispname + '</a>';
@ -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 = '<td class="role">' + select + '</td>' +
'<td class="name">' + dispname + '</td>' +
'<td class="availability"><img src="./program/resources/blank.gif" class="availabilityicon ' + avail + '" /></td>' +
'<td class="availability"><img src="./program/resources/blank.gif" class="availabilityicon ' + avail + '" data-email="' + data.email + '" /></td>' +
'<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '" title="' + Q(data.status || '') + '">' + Q(data.status || '') + '</span></td>' +
'<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
var table = calendar.resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
var tr = $('<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($('<tr>').addClass(k)
.append('<td class="title">' + Q(ucfirst(rcmail.get_label(k, 'calendar'))) + '</td>')
.append('<td class="value">' + text2html(render_attrib(attribs[k])) + '</td>')
);
}
$(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 = $('<a>').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($('<tr>').addClass(k)
.append('<td class="title">' + Q(ucfirst(rcmail.get_label('owner'+k, 'calendar'))) + '</td>')
.append('<td class="value">' + text2html(data[k]) + '</td>')
);
}
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

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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;
}
/**
*
*/

View file

@ -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';

View file

@ -1,13 +1,13 @@
<div id="eventedit" class="uidialog">
<form id="eventtabs" action="#" method="post" enctype="multipart/form-data">
<ul>
<li><a href="#event-tab-1"><roundcube:label name="calendar.tabsummary" /></a></li>
<li id="edit-tab-recurrence"><a href="#event-tab-2"><roundcube:label name="calendar.tabrecurrence" /></a></li>
<li id="edit-tab-attendees"><a href="#event-tab-3"><roundcube:label name="calendar.tabattendees" /></a></li>
<li id="edit-tab-attachments"><a href="#event-tab-4"><roundcube:label name="calendar.tabattachments" /></a></li>
<li><a href="#event-panel-1"><roundcube:label name="calendar.tabsummary" /></a></li>
<li id="edit-tab-recurrence"><a href="#event-panel-recurrence"><roundcube:label name="calendar.tabrecurrence" /></a></li>
<li id="edit-tab-attendees"><a href="#event-panel-attendees"><roundcube:label name="calendar.tabattendees" /></a></li>
<li id="edit-tab-attachments"><a href="#event-panel-attachments"><roundcube:label name="calendar.tabattachments" /></a></li>
</ul>
<!-- basic info -->
<div id="event-tab-1">
<div id="event-panel-1">
<div class="event-section">
<label for="edit-title"><roundcube:label name="calendar.title" /></label>
<br />
@ -65,7 +65,7 @@
</div>
</div>
<!-- recurrence settings -->
<div id="event-tab-2">
<div id="event-panel-recurrence">
<div class="event-section border-after">
<roundcube:object name="plugin.recurrence_form" part="frequency" />
</div>
@ -86,13 +86,13 @@
</div>
</div>
<!-- attendees list -->
<div id="event-tab-3">
<div id="event-panel-attendees">
<roundcube:object name="plugin.attendees_list" id="edit-attendees-table" cellspacing="0" cellpadding="0" border="0" />
<roundcube:object name="plugin.attendees_form" id="edit-attendees-form" />
<roundcube:include file="/templates/freebusylegend.html" />
</div>
<!-- attachments list (with upload form) -->
<div id="event-tab-4">
<div id="event-panel-attachments">
<div id="edit-attachments" class="attachments-list">
<roundcube:object name="plugin.attachments_list" id="attachmentlist" deleteIcon="/images/icons/delete.png" cancelIcon="/images/icons/delete.png" loadingIcon="/images/display/loading_blue.gif" />
</div>

View file

@ -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 {

View file

@ -113,6 +113,45 @@
<roundcube:include file="/templates/eventedit.html" />
<div id="eventresourcesdialog" class="uidialog">
<div id="resource-dialog-left">
<div id="resource-selection" class="uibox listbox">
<div id="resourcequicksearch">
<div class="searchbox">
<roundcube:object name="plugin.resources_searchform" id="resourcesearchbox" />
<a id="resourcesearchmenulink" class="iconbutton searchoptions"> </a>
<roundcube:button command="reset-resource-search" id="resourcesearchreset" class="iconbutton reset" title="resetsearch" content=" " />
</div>
</div>
<div class="scroller">
<roundcube:object name="plugin.resources_list" id="resources-list" class="listing treelist" />
</div>
<!--
<div class="boxpagenav">
<roundcube:button command="firstpage" type="link" class="icon firstpage disabled" classAct="icon firstpage" title="firstpage" content="|&amp;lt;" />
<roundcube:button command="previouspage" type="link" class="icon prevpage disabled" classAct="icon prevpage" title="previouspage" content="&amp;lt;" />
<roundcube:button command="nextpage" type="link" class="icon nextpage disabled" classAct="icon nextpage" title="nextpage" content="&amp;gt;" />
<roundcube:button command="lastpage" type="link" class="icon lastpage disabled" classAct="icon lastpage" title="lastpage" content="&amp;gt;|" />
</div>
-->
</div>
</div>
<div id="resource-dialog-right">
<div id="resource-info" class="uibox contentbox">
<h2 class="boxtitle"><roundcube:label name="calendar.resourcedetails" /></h2>
<div class="scroller">
<roundcube:object name="plugin.resource_info" id="resource-details" class="propform" />
</div>
</div>
<div id="resource-availability" class="uibox contentbox">
<h2 class="boxtitle"><roundcube:label name="calendar.resourceavailability" /></h2>
<div id="resource-freebusy-calendar"></div>
</div>
</div>
</div>
<div id="eventfreebusy" class="uidialog">
<roundcube:object name="plugin.attendees_freebusy_table" id="attendees-freebusy-table" cellpadding="0" />
@ -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();
});
</script>

View file

@ -1,10 +1,10 @@
<div id="eventedit" class="uidialog uidialog-tabbed">
<form id="eventtabs" action="#" method="post" enctype="multipart/form-data">
<ul>
<li><a href="#event-tab-1"><roundcube:label name="calendar.tabsummary" /></a></li><li id="edit-tab-recurrence"><a href="#event-tab-2"><roundcube:label name="calendar.tabrecurrence" /></a></li><li id="edit-tab-attendees"><a href="#event-tab-3"><roundcube:label name="calendar.tabattendees" /></a></li><li id="edit-tab-attachments"><a href="#event-tab-4"><roundcube:label name="calendar.tabattachments" /></a></li>
<li><a href="#event-panel-summary"><roundcube:label name="calendar.tabsummary" /></a></li><li id="edit-tab-recurrence"><a href="#event-panel-recurrence"><roundcube:label name="calendar.tabrecurrence" /></a></li><li id="edit-tab-attendees"><a href="#event-panel-attendees"><roundcube:label name="calendar.tabattendees" /></a></li><li id="edit-tab-resources"><a href="#event-panel-resources"><roundcube:label name="calendar.tabresources" /></a></li><li id="edit-tab-attachments"><a href="#event-panel-attachments"><roundcube:label name="calendar.tabattachments" /></a></li>
</ul>
<!-- basic info -->
<div id="event-tab-1">
<div id="event-panel-summary">
<div class="event-section">
<label for="edit-title"><roundcube:label name="calendar.title" /></label>
<br />
@ -62,7 +62,7 @@
</div>
</div>
<!-- recurrence settings -->
<div id="event-tab-2">
<div id="event-panel-recurrence">
<div class="event-section border-after">
<roundcube:object name="plugin.recurrence_form" part="frequency" />
</div>
@ -83,20 +83,26 @@
</div>
</div>
<!-- attendees list -->
<div id="event-tab-3">
<roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="records-table" />
<div id="event-panel-attendees">
<roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="records-table edit-attendees-table" coltitle="attendee" />
<roundcube:object name="plugin.attendees_form" id="edit-attendees-form" />
<roundcube:include file="/templates/freebusylegend.html" />
</div>
<!-- resources list -->
<div id="event-panel-resources">
<roundcube:object name="plugin.attendees_list" id="edit-resources-table" class="records-table edit-attendees-table" coltitle="resource" />
<roundcube:object name="plugin.resources_form" id="edit-resources-form" />
<roundcube:include file="/templates/freebusylegend.html" />
</div>
<!-- attachments list (with upload form) -->
<div id="event-tab-4">
<div id="event-panel-attachments">
<div id="edit-attachments">
<roundcube:object name="plugin.attachments_list" id="attachment-list" class="attachmentslist" />
</div>
<div id="edit-attachments-form">
<roundcube:object name="plugin.attachments_form" id="calendar-attachment-form" attachmentFieldSize="30" />
</div>
<roundcube:object name="plugin.filedroparea" id="event-tab-4" />
<roundcube:object name="plugin.filedroparea" id="event-panel-attachments" />
</div>
</form>