Refactored free-busy display

This commit is contained in:
Thomas Bruederli 2011-07-20 19:15:21 +02:00
parent 9a96600ddd
commit c80dea2330
6 changed files with 168 additions and 56 deletions

View file

@ -1235,11 +1235,15 @@ class calendar extends rcube_plugin
{ {
$email = get_input_value('email', RCUBE_INPUT_GPC); $email = get_input_value('email', RCUBE_INPUT_GPC);
$start = get_input_value('start', RCUBE_INPUT_GET); $start = get_input_value('start', RCUBE_INPUT_GET);
$end = get_input_value('end', RCUBE_INPUT_GET); # $end = get_input_value('end', RCUBE_INPUT_GET);
$interval = 60; // in minutes $interval = 3600; // in seconds
if (!$start) $start = time(); if (!$start) $start = time();
if (!$end) $end = $start + 86400 * 7; if (!$end) $end = $start + 86400 * 30;
// reset $start/$end to midnight
#$start = gmmktime(0, 0, 0, gmdate('n', $start), gmdate('j'), gmdate('Y'));
#$end = gmmktime(23, 59, 59, gmdate('n', $end), gmdate('j'), gmdate('Y'));
$fblist = $this->driver->get_freebusy_list($email, $start, $end); $fblist = $this->driver->get_freebusy_list($email, $start, $end);
$slots = array(); $slots = array();
@ -1247,7 +1251,7 @@ class calendar extends rcube_plugin
// 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++) { for ($s = 0, $t = $start; $t <= $end; $s++) {
$status = self::FREEBUSY_UNKNOWN; $status = self::FREEBUSY_UNKNOWN;
$t_end = $t + $interval*60; $t_end = $t + $interval;
// determine attendee's status // determine attendee's status
if (is_array($fblist)) { if (is_array($fblist)) {
@ -1265,7 +1269,7 @@ class calendar extends rcube_plugin
$t = $t_end; $t = $t_end;
} }
echo json_encode(array('email' => $email, 'slots' => $slots, 'interval' => $interval)); echo json_encode(array('email' => $email, 'start' => intval($start), 'interval' => $interval, 'slots' => $slots));
exit; exit;
} }

View file

@ -44,6 +44,8 @@ function rcube_calendar_ui(settings)
var ignore_click = false; var ignore_click = false;
var event_attendees = null; var event_attendees = null;
var attendees_list; var attendees_list;
var freebusy_ui = {};
var freebusy_data = {};
// general datepicker settings // general datepicker settings
var datepicker_settings = { var datepicker_settings = {
@ -615,52 +617,39 @@ function rcube_calendar_ui(settings)
var endtime = $('#schedule-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show(); var endtime = $('#schedule-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show();
var allday = $('#edit-allday').get(0); var allday = $('#edit-allday').get(0);
if (event.allDay) { if (allday.checked) {
starttime.val("00:00").hide(); starttime.val("00:00").hide();
endtime.val("23:59").hide(); endtime.val("23:59").hide();
} }
// render time slots // render time slots
var now = new Date(), range_start = new Date(), range_end = new Date(); var interval = 60;
range_start.setTime(Math.max(now, event.start.getTime() - 86400000 * 3)); // start 3 days before event var now = new Date(), fb_start = new Date(), fb_end = new Date();
range_start.setHours(0); fb_start.setTime(Math.max(now, event.start));
range_start.setMinutes(0); fb_start.setHours(0);
range_start.setSeconds(0); fb_start.setMinutes(0);
range_end.setTime(range_start.getTime() + 86400000 * 10); // show 10 days fb_start.setSeconds(0);
fb_start.setMilliseconds(0);
fb_end.setTime(fb_start.getTime() + 86400000);
var interval = 60; // minutes freebusy_data = {};
var dayslots = 24; freebusy_ui.loading = 1;
var lastdate, datestr, curdate = new Date(), dates_row = '<tr class="dates">', times_row = '<tr class="times">', slots_row = ''; freebusy_ui.numdays = 1;
for (var s = 0, t = range_start.getTime(); t < range_end.getTime(); s++) { freebusy_ui.interval = interval;
curdate.setTime(t); freebusy_ui.start = fb_start;
datestr = $.fullCalendar.formatDate(curdate, settings['date_format']); freebusy_ui.end = new Date(freebusy_ui.start.getTime() + 86400000 * freebusy_ui.numdays);
if (datestr != lastdate) { render_freebusy_grid(0);
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 // render list of attendees
var domid, dispname, data, list_html = '', times_html = ''; var domid, dispname, data, list_html = '';
for (var i=0; i < event_attendees.length; i++) { for (var i=0; i < event_attendees.length; i++) {
data = event_attendees[i]; data = event_attendees[i];
dispname = Q(data.name || data.email); dispname = Q(data.name || data.email);
domid = String(data.email).replace(rcmail.identifier_expr, ''); domid = String(data.email).replace(rcmail.identifier_expr, '');
list_html += '<div class="attendee ' + String(data.role).toLowerCase() + '" id="rcmli' + domid + '">' + dispname + '</div>'; 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-attendees-list').html(list_html);
$('#schedule-freebusy-times > thead').html(dates_row + times_row);
$('#schedule-freebusy-times > tbody').html(times_html);
// dialog buttons // dialog buttons
var buttons = {}; var buttons = {};
@ -680,9 +669,71 @@ function rcube_calendar_ui(settings)
buttons: buttons, buttons: buttons,
minWidth: 640, minWidth: 640,
width: 850 width: 850
}).show(); }).show();
// fetch data from server
freebusy_ui.loading = 0;
load_freebusy_data(fb_start, interval);
};
// render an HTML table showing free-busy status for all the event attendees
var render_freebusy_grid = function(delta)
{
if (delta) {
freebusy_ui.start.setTime(freebusy_ui.start.getTime() + 86400000 * delta);
freebusy_ui.end = new Date(freebusy_ui.start.getTime() + 86400000 * freebusy_ui.numdays);
}
var dayslots = Math.floor(1440 / freebusy_ui.interval);
var lastdate, datestr, css, curdate = new Date(), dates_row = '<tr class="dates">', times_row = '<tr class="times">', slots_row = '';
for (var s = 0, t = freebusy_ui.start.getTime(); t < freebusy_ui.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;
}
// TODO: define working hours by config
css = !(curdate.getHours() >= 6 && curdate.getHours() <= 18) ? 'offhours' : 'workinghours';
times_row += '<td class="' + css + '">' + Q($.fullCalendar.formatDate(curdate, settings['time_format'])) + '</td>';
slots_row += '<td class="' + css + ' unknown">&nbsp;</td>';
t += freebusy_ui.interval * 60000;
}
dates_row += '</tr>';
times_row += '</tr>';
// render list of attendees
var domid, data, list_html = '', times_html = '';
for (var i=0; i < event_attendees.length; i++) {
data = event_attendees[i];
domid = String(data.email).replace(rcmail.identifier_expr, '');
times_html += '<tr id="fbrow' + domid + '">' + slots_row + '</tr>';
}
$('#schedule-freebusy-times > thead').html(dates_row + times_row);
$('#schedule-freebusy-times > tbody').html(times_html);
// if we have loaded free-busy data, show it
if (!freebusy_ui.loading) {
if (date2unixtime(freebusy_ui.start) < freebusy_data.start || date2unixtime(freebusy_ui.end) > freebusy_data.end) {
load_freebusy_data(freebusy_ui.start, freebusy_ui.interval)
return;
}
for (var email, i=0; i < event_attendees.length; i++) {
if ((email = event_attendees[i].email))
update_freebusy_display(email);
}
}
};
// fetch free-busy information for each attendee from server
var load_freebusy_data = function(from, interval)
{
var start = new Date(from.getTime() - 86400000 * 2); // start 1 days before event
var end = new Date(start.getTime() + 86400000 * 14); // load 14 days
// load free-busy information for every attendee // load free-busy information for every attendee
var domid, email var domid, email
@ -690,35 +741,56 @@ function rcube_calendar_ui(settings)
if ((email = event_attendees[i].email)) { if ((email = event_attendees[i].email)) {
domid = String(email).replace(rcmail.identifier_expr, ''); domid = String(email).replace(rcmail.identifier_expr, '');
$('#rcmli' + domid).addClass('loading'); $('#rcmli' + domid).addClass('loading');
freebusy_ui.loading++;
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
dataType: 'json', dataType: 'json',
url: rcmail.url('freebusy-times'), url: rcmail.url('freebusy-times'),
data: { email:email, start:date2unixtime(range_start), end:date2unixtime(range_end), _remote: 1 }, data: { email:email, start:date2unixtime(start), end:date2unixtime(end), _remote: 1 },
success: function(data){ success: function(data){
var status_classes = ['unknown','free','busy','tentative','out-of-office']; freebusy_ui.loading--;
// copy data to member var
var ts = data.start - 0;
freebusy_data.start = ts;
freebusy_data[data.email] = {};
for (var i=0; i < data.slots.length; i++) {
freebusy_data[data.email][ts] = data.slots[i];
ts += data.interval;
}
freebusy_data.end = ts;
// hide loading indicator
var domid = String(data.email).replace(rcmail.identifier_expr, ''); var domid = String(data.email).replace(rcmail.identifier_expr, '');
$('#rcmli' + domid).removeClass('loading'); $('#rcmli' + domid).removeClass('loading');
var row = $('#fbrow' + domid); // update display
if (data.slots && row.length) { update_freebusy_display(data.email);
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 free-busy grid with status loaded from server
var update_freebusy_display = function(email)
{
var status_classes = ['unknown','free','busy','tentative','out-of-office'];
var domid = String(email).replace(rcmail.identifier_expr, '');
var row = $('#fbrow' + domid);
var ts = date2unixtime(freebusy_ui.start);
var fbdata = freebusy_data[email];
if (fbdata && fbdata[ts] && row.length) {
row.children().each(function(i, cell){
cell.className = cell.className.replace('unknown', fbdata[ts] ? status_classes[fbdata[ts]] : 'unknown');
ts += freebusy_ui.interval * 60;
});
}
};
// update event properties and attendees availability if event times have changed // update event properties and attendees availability if event times have changed
var event_times_changed = function() var event_times_changed = function()
{ {
@ -1524,6 +1596,15 @@ function rcube_calendar_ui(settings)
event_freebusy_dialog(); event_freebusy_dialog();
}); });
$('#shedule-freebusy-prev').button().click(function(){ render_freebusy_grid(-1); });
$('#shedule-freebusy-next').button().click(function(){ render_freebusy_grid(1); }).parent().buttonset();
$('#schedule-freebusy-wokinghours').click(function(){
$('#workinghourscss').remove();
if (this.checked)
$('<style type="text/css" id="workinghourscss"> td.offhours { display:none } </style>').appendTo('head');
});
// add proprietary css styles if not IE // add proprietary css styles if not IE
if (!bw.ie) if (!bw.ie)
$('div.fc-content').addClass('rcube-fc-content'); $('div.fc-content').addClass('rcube-fc-content');

View file

@ -198,6 +198,9 @@ class kolab_calendar
{ {
$this->_fetch_events(); $this->_fetch_events();
if (!empty($search))
$search = mb_strtolower($search);
$events = array(); $events = array();
foreach ($this->events as $id => $event) { foreach ($this->events as $id => $event) {
// filter events by search query // filter events by search query

View file

@ -98,6 +98,7 @@ $labels['availunknown'] = 'Unknown';
$labels['availoutofoffice'] = 'Out of Office'; $labels['availoutofoffice'] = 'Out of Office';
$labels['scheduletime'] = 'Available times'; $labels['scheduletime'] = 'Available times';
$labels['sendnotifications'] = 'Send notifications'; $labels['sendnotifications'] = 'Send notifications';
$labels['onlyworkinghours'] = 'Show only working hours';
// event dialog tabs // event dialog tabs
$labels['tabsummary'] = 'Summary'; $labels['tabsummary'] = 'Summary';

View file

@ -634,7 +634,7 @@ td.topalign {
width: 100%; width: 100%;
table-layout: fixed; table-layout: fixed;
border-collapse: collapse; border-collapse: collapse;
margin-bottom: 2em; margin: 0.5em 0;
} }
#attendees-freebusy-table td.attendees { #attendees-freebusy-table td.attendees {
@ -683,12 +683,27 @@ td.topalign {
} }
#schedule-freebusy-times tr.dates th { #schedule-freebusy-times tr.dates th {
border-bottom: 0; border-color: #aaa;
border-style: solid;
border-width: 0 1px 0 1px;
} }
#schedule-freebusy-times tr.times td { #schedule-freebusy-times tr.times td {
min-width: 30px;
font-size: 9px; font-size: 9px;
padding: 5px 2px 6px 2px; padding: 5px 2px 6px 2px;
text-align: center;
}
#eventfreebusy .schedule-options {
position: relative;
margin-bottom: 2em;
}
#eventfreebusy .schedule-buttons {
position: absolute;
top: 0;
right: 0;
} }
span.edit-alarm-set { span.edit-alarm-set {

View file

@ -187,6 +187,14 @@
<div id="eventfreebusy" class="uidialog"> <div id="eventfreebusy" class="uidialog">
<roundcube:object name="plugin.attendees_freebusy_table" id="attendees-freebusy-table" cellspacing="0" cellpadding="0" border="0" /> <roundcube:object name="plugin.attendees_freebusy_table" id="attendees-freebusy-table" cellspacing="0" cellpadding="0" border="0" />
<div class="schedule-options">
<label><input type="checkbox" id="schedule-freebusy-wokinghours" value="1" /><roundcube:label name="calendar.onlyworkinghours" /></label>
<div class="schedule-buttons">
<button id="shedule-freebusy-prev" title="<roundcube:label name='previouspage' />">&#9668;</button>
<button id="shedule-freebusy-next" title="<roundcube:label name='nextpage' />">&#9658;</button>
</div>
</div>
<div class="form-section"> <div class="form-section">
<label for="schedule-startdate"><roundcube:label name="calendar.start" /></label> <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="startdate" size="10" id="schedule-startdate" disabled="true" /> &nbsp;