Show free/busy times for event attendess (work-in-progress)

This commit is contained in:
Thomas Bruederli 2011-07-20 14:46:38 +02:00
parent 0ec7458eb1
commit 028f3069d6
8 changed files with 314 additions and 44 deletions

View file

@ -23,8 +23,14 @@
+-------------------------------------------------------------------------+
*/
class calendar extends rcube_plugin
{
const FREEBUSY_UNKNOWN = 0;
const FREEBUSY_FREE = 1;
const FREEBUSY_BUSY = 2;
const FREEBUSY_OOF = 4;
public $task = '?(?!login|logout).*';
public $rc;
public $driver;
@ -181,6 +187,7 @@ class calendar extends rcube_plugin
$this->register_handler('plugin.attachments_list', array($this->ui, 'attachments_list'));
$this->register_handler('plugin.attendees_list', array($this->ui, 'attendees_list'));
$this->register_handler('plugin.attendees_form', array($this->ui, 'attendees_form'));
$this->register_handler('plugin.attendees_freebusy_table', array($this->ui, 'attendees_freebusy_table'));
$this->register_handler('plugin.edit_recurring_warning', array($this->ui, 'recurring_event_warning'));
$this->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); // use generic method from rcube_template
@ -1207,16 +1214,36 @@ class calendar extends rcube_plugin
$email = get_input_value('email', RCUBE_INPUT_GPC);
$start = get_input_value('start', RCUBE_INPUT_GET);
$end = get_input_value('end', RCUBE_INPUT_GET);
$interval = 60; // in minutes
if (!$start) $start = time();
if (!$end) $end = $start + 86400 * 30;
if (!$end) $end = $start + 86400 * 7;
$fblist = $this->driver->get_freebusy_list($email, $start, $end);
$result = array();
$slots = array();
// TODO: build a list from $start till $end with blocks representing the fb-status
// build a list from $start till $end with blocks representing the fb-status
for ($s = 0, $t = $start; $t <= $end; $s++) {
$status = self::FREEBUSY_UNKNOWN;
$t_end = $t + $interval*60;
// determine attendee's status
if (is_array($fblist)) {
$status = self::FREEBUSY_FREE;
foreach ($fblist as $slot) {
list($from, $to) = $slot;
if ($from <= $t_end && $to > $t) {
$status = self::FREEBUSY_BUSY;
break;
}
}
}
$slots[$s] = $status;
$t = $t_end;
}
echo json_encode($result);
echo json_encode(array('email' => $email, 'slots' => $slots, 'interval' => $interval));
exit;
}

View file

@ -261,15 +261,17 @@ function rcube_calendar_ui(settings)
// list event attendees
if (calendar.attendees && event.attendees) {
var data, dispname, html = '';
var data, dispname, organizer = false, html = '';
for (var j=0; j < event.attendees.length; j++) {
data = event.attendees[j];
dispname = Q(data.name || data.email);
if (data.email)
dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink">' + dispname + '</a>';
html += '<span class="attendee ' + String(data.status).toLowerCase() + '">' + dispname + '</span> ';
html += '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '">' + dispname + '</span> ';
if (data.role == 'ORGANIZER')
organizer = true;
}
if (html) {
if (html && event.attendees.length > 1 || !organizer) {
$('#event-attendees').show()
.children('.event-text')
.html(html)
@ -317,26 +319,6 @@ function rcube_calendar_ui(settings)
};
//jquery-ui dialog for printing calendars - stub
var calendars_print_dialog = function(action, event)
{
var $dialog = $("#printcalendar");
$dialog.dialog({
modal: true,
resizable: true,
closeOnEscape: false,
title: rcmail.gettext('Print', 'calendar'),
close: function() {
$dialog.dialog("destroy").hide();
},
//buttons: buttons,
minWidth: 500,
width: 580
}).show();
}
// bring up the event dialog (jquery-ui popup)
var event_edit_dialog = function(action, event)
{
@ -621,6 +603,123 @@ function rcube_calendar_ui(settings)
title.select();
};
var event_freebusy_dialog = function()
{
var $dialog = $('#eventfreebusy').dialog('close');
var event = me.selected_event;
// set form elements
var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000);
var startdate = $('#schedule-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration);
var starttime = $('#schedule-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show();
var enddate = $('#schedule-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format']));
var endtime = $('#schedule-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show();
var allday = $('#edit-allday').get(0);
if (event.allDay) {
starttime.val("00:00").hide();
endtime.val("23:59").hide();
}
// render time slots
var now = new Date(), range_start = new Date(), range_end = new Date();
range_start.setTime(Math.max(now, event.start.getTime() - 86400000 * 3)); // start 3 days before event
range_start.setHours(0);
range_start.setMinutes(0);
range_start.setSeconds(0);
range_end.setTime(range_start.getTime() + 86400000 * 10); // show 10 days
var interval = 60; // minutes
var dayslots = 24;
var lastdate, datestr, curdate = new Date(), dates_row = '<tr class="dates">', times_row = '<tr class="times">', slots_row = '';
for (var s = 0, t = range_start.getTime(); t < range_end.getTime(); s++) {
curdate.setTime(t);
datestr = $.fullCalendar.formatDate(curdate, settings['date_format']);
if (datestr != lastdate) {
dates_row += '<th colspan="' + dayslots + '" class="boxtitle date' + $.fullCalendar.formatDate(curdate, 'ddMMyyyy') + '">' + Q(datestr) + '</th>';
lastdate = datestr;
}
times_row += '<td>' + Q($.fullCalendar.formatDate(curdate, settings['time_format'])) + '</td>';
slots_row += '<td class="unknown">&nbsp;</td>';
t += interval * 60000;
}
dates_row += '</tr>';
times_row += '</tr>';
// render list of attendees
var domid, dispname, data, list_html = '', times_html = '';
for (var i=0; i < event_attendees.length; i++) {
data = event_attendees[i];
dispname = Q(data.name || data.email);
domid = String(data.email).replace(rcmail.identifier_expr, '');
list_html += '<div class="attendee ' + String(data.role).toLowerCase() + '" id="rcmli' + domid + '">' + dispname + '</div>';
times_html += '<tr id="fbrow' + domid + '">' + slots_row + '</tr>';
}
$('#schedule-attendees-list').html(list_html);
$('#schedule-freebusy-times > thead').html(dates_row + times_row);
$('#schedule-freebusy-times > tbody').html(times_html);
// dialog buttons
var buttons = {};
buttons[rcmail.gettext('cancel', 'calendar')] = function() {
$dialog.dialog("close");
};
$dialog.dialog({
modal: true,
resizable: true,
closeOnEscape: true,
title: rcmail.gettext('scheduletime', 'calendar'),
close: function() {
$dialog.dialog("destroy").hide();
},
buttons: buttons,
minWidth: 640,
width: 850
}).show();
// load free-busy information for every attendee
var domid, email
for (var i=0; i < event_attendees.length; i++) {
if ((email = event_attendees[i].email)) {
domid = String(email).replace(rcmail.identifier_expr, '');
$('#rcmli' + domid).addClass('loading');
$.ajax({
type: 'GET',
dataType: 'json',
url: rcmail.url('freebusy-times'),
data: { email:email, start:date2unixtime(range_start), end:date2unixtime(range_end), _remote: 1 },
success: function(data){
var status_classes = ['unknown','free','busy','tentative','out-of-office'];
var domid = String(data.email).replace(rcmail.identifier_expr, '');
$('#rcmli' + domid).removeClass('loading');
var row = $('#fbrow' + domid);
if (data.slots && row.length) {
row.children().each(function(i, cell){
cell.className = data.slots[i] ? status_classes[data.slots[i]] : 'unknown';
});
}
}
});
}
}
// scroll to event date
var pos = $('#schedule-freebusy-times th.date' + $.fullCalendar.formatDate(event.start, 'ddMMyyyy')).position();
if (pos && pos.left)
$('#schedule-freebusy-times').parent().scrollLeft(pos.left);
};
// update event properties and attendees availability if event times have changed
var event_times_changed = function()
{
@ -1421,6 +1520,10 @@ function rcube_calendar_ui(settings)
if (add_attendees(input.val()))
input.val('');
});
$('#edit-attendee-schedule').click(function(){
event_freebusy_dialog();
});
// add proprietary css styles if not IE
if (!bw.ie)

View file

@ -572,9 +572,32 @@ class calendar_ui
function attendees_form($attrib = array())
{
$input = new html_inputfield(array('name' => 'participant', 'id' => 'edit-attendee-name', 'size' => 30));
$checkbox = new html_checkbox(array('name' => 'notify', 'id' => 'edit-attendees-notify', 'value' => 1, 'disabled' => true)); // disabled for now
return html::div($attrib, $input->show() . " " .
html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->calendar->gettext('addattendee'))));
return html::div($attrib,
html::div(null, $input->show() . " " .
html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->calendar->gettext('addattendee'))) . " " .
html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-schedule', 'value' => $this->calendar->gettext('scheduletime').'...'))) .
html::p('attendees-notifybox', html::label(null, $checkbox->show(1) . $this->calendar->gettext('sendnotifications')))
);
}
/**
*
*/
function attendees_freebusy_table($attrib = array())
{
$table = new html_table(array('cols' => 2, 'border' => 0, 'cellspacing' => 0));
$table->add('attendees',
html::tag('h3', 'boxtitle', $this->calendar->gettext('tabattendees')) .
html::div('timesheader', '&nbsp;') .
html::div(array('id' => 'schedule-attendees-list'), '')
);
$table->add('times',
html::div('scroll', html::tag('table', array('id' => 'schedule-freebusy-times', 'border' => 0, 'cellspacing' => 0), html::tag('thead') . html::tag('tbody')))
);
return $table->show($attrib);
}
}

View file

@ -92,6 +92,12 @@ $labels['roleorganizer'] = 'Organizer';
$labels['rolerequired'] = 'Required';
$labels['roleoptional'] = 'Optional';
$labels['roleresource'] = 'Resource';
$labels['availfree'] = 'Free';
$labels['availbusy'] = 'Busy';
$labels['availunknown'] = 'Unknown';
$labels['availoutofoffice'] = 'Out of Office';
$labels['scheduletime'] = 'Available times';
$labels['sendnotifications'] = 'Send notifications';
// event dialog tabs
$labels['tabsummary'] = 'Summary';

View file

@ -235,9 +235,7 @@ pre {
right: 4px;
}
#eventshow,
#eventedit,
#calendarform {
div.uidialog {
display: none;
}
@ -363,6 +361,10 @@ a.miniColors-trigger {
background-position: right -60px;
}
#event-attendees span.organizer {
background-position: right -80px;
}
/* jQuery UI overrides */
#eventshow h1 {
@ -503,12 +505,12 @@ td.topalign {
display: table;
table-layout: fixed;
border-collapse: collapse;
border: 1px solid #ddd;
border: 1px solid #ccc;
}
#edit-attendees-table td {
padding: 3px;
border-bottom: 1px solid #ddd;
border-bottom: 1px solid #ccc;
}
#edit-attendees-table td.role {
@ -538,16 +540,23 @@ td.topalign {
}
#edit-attendees-form {
position: relative;
margin-top: 1em;
}
#edit-attendees-form #edit-attendee-schedule {
position: absolute;
top: 0;
right: 0;
}
#edit-attendees-table select.edit-attendee-role {
border: 0;
padding: 2px;
background: white;
}
#edit-attendees-table img.availabilityicon {
.availability img.availabilityicon {
margin: 1px;
width: 14px;
height: 14px;
@ -555,26 +564,43 @@ td.topalign {
-moz-border-radius: 4px;
}
#edit-attendees-table img.availabilityicon.loading {
.availability img.availabilityicon.loading {
background: url('images/loading-small.gif') top left no-repeat;
}
#edit-attendees-table img.availabilityicon.unknown {
background: #ccc;
#schedule-freebusy-times td.unknown,
.availability img.availabilityicon.unknown {
background: #ddd;
}
#edit-attendees-table img.availabilityicon.free {
#schedule-freebusy-times td.free,
.availability img.availabilityicon.free {
background: #0c0;
}
#edit-attendees-table img.availabilityicon.busy {
#schedule-freebusy-times td.busy,
.availability img.availabilityicon.busy {
background: #c00;
}
#edit-attendees-table img.availabilityicon.out-of-office {
#schedule-freebusy-times td.out-of-office,
.availability img.availabilityicon.out-of-office {
background: #f0b400;
}
#edit-attendees-legend {
margin-top: 3em;
}
#edit-attendees-legend .legend {
margin-right: 2em;
white-space: nowrap;
}
#edit-attendees-legend img.availabilityicon {
vertical-align: middle;
}
#edit-attendees-table tbody td.confirmstate {
overflow: hidden;
white-space: nowrap;
@ -603,6 +629,67 @@ td.topalign {
background-position: 5px -60px;
}
#attendees-freebusy-table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
margin-bottom: 2em;
}
#attendees-freebusy-table td.attendees {
width: 16em;
border: 1px solid #ccc;
vertical-align: top;
overflow: hidden;
}
#attendees-freebusy-table td.times {
width: auto;
vertical-align: top;
border: 1px solid #ccc;
}
#attendees-freebusy-table div.scroll {
overflow: auto;
}
#attendees-freebusy-table h3.boxtitle {
margin: 0;
height: auto !important;
border-color: #ccc;
}
#attendees-freebusy-table div.timesheader {
padding: 0.3em;
}
#schedule-attendees-list div.attendee {
padding: 3px 20px 3px 6px;
border-top: 1px solid #ccc;
}
#schedule-attendees-list div.loading {
background: url('images/loading-small.gif') top right no-repeat;
}
#schedule-freebusy-times {
border-collapse: collapse;
}
#schedule-freebusy-times td {
padding: 3px;
border: 1px solid #ccc;
}
#schedule-freebusy-times tr.dates th {
border-bottom: 0;
}
#schedule-freebusy-times tr.times td {
font-size: 9px;
padding: 5px 2px 6px 2px;
}
span.edit-alarm-set {
white-space: nowrap;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -36,7 +36,7 @@
</ul>
</div>
<div id="eventshow">
<div id="eventshow" class="uidialog">
<h1 id="event-title">Event Title</h1>
<div class="event-section" id="event-location">Location</div>
<div class="event-section" id="event-date">From-To</div>
@ -82,7 +82,7 @@
</div>
</div>
<div id="eventedit">
<div id="eventedit" class="uidialog">
<form id="eventtabs" action="#" method="post">
<ul>
<li><a href="#event-tab-1"><roundcube:label name="calendar.tabsummary" /></a></li>
@ -168,6 +168,7 @@
<div id="event-tab-3">
<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">
@ -183,7 +184,24 @@
<roundcube:object name="plugin.edit_recurring_warning" class="edit-recurring-warning" style="display:none" />
</div>
<div id="calendarform">
<div id="eventfreebusy" class="uidialog">
<roundcube:object name="plugin.attendees_freebusy_table" id="attendees-freebusy-table" cellspacing="0" cellpadding="0" border="0" />
<div class="form-section">
<label for="schedule-startdate"><roundcube:label name="calendar.start" /></label>
<input type="text" name="startdate" size="10" id="schedule-startdate" disabled="true" /> &nbsp;
<input type="text" name="starttime" size="6" id="schedule-starttime" disabled="true" />
</div>
<div class="form-section">
<label for="schedule-enddate"><roundcube:label name="calendar.end" /></label>
<input type="text" name="enddate" size="10" id="schedule-enddate" disabled="true" /> &nbsp;
<input type="text" name="endtime" size="6" id="schedule-endtime" disabled="true" />
</div>
<roundcube:include file="/templates/freebusylegend.html" />
</div>
<div id="calendarform" class="uidialog">
<roundcube:label name="loading" />
</div>

View file

@ -0,0 +1,6 @@
<div id="edit-attendees-legend" class="availability">
<span class="legend"><img class="availabilityicon free" src="./program/blank.gif" /> <roundcube:label name="calendar.availfree" /></span>
<span class="legend"><img class="availabilityicon busy" src="./program/blank.gif" /> <roundcube:label name="calendar.availbusy" /></span>
<span class="legend"><img class="availabilityicon out-of-office" src="./program/blank.gif" /> <roundcube:label name="calendar.availoutofoffice" /></span>
<span class="legend"><img class="availabilityicon unknown" src="./program/blank.gif" /> <roundcube:label name="calendar.availunknown" /></span>
</div>