Implement finding of free time slots

This commit is contained in:
Thomas Bruederli 2011-07-24 12:46:54 +02:00
parent dd5f8e56bd
commit ad5a5c6e84
8 changed files with 193 additions and 16 deletions

View file

@ -52,6 +52,8 @@ class calendar extends rcube_plugin
'calendar_timeslots' => 2, 'calendar_timeslots' => 2,
'calendar_first_day' => 1, 'calendar_first_day' => 1,
'calendar_first_hour' => 6, 'calendar_first_hour' => 6,
'calendar_work_start' => 6,
'calendar_work_end' => 18,
); );
private $default_categories = array( private $default_categories = array(
@ -627,6 +629,8 @@ class calendar extends rcube_plugin
$settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']); $settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']);
$settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']); $settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
$settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']); $settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']);
$settings['work_start'] = (int)$this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']);
$settings['work_end'] = (int)$this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']);
$settings['timezone'] = $this->timezone; $settings['timezone'] = $this->timezone;
// localization // localization

View file

@ -44,7 +44,7 @@ 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_ui = { workinhoursonly:false };
var freebusy_data = {}; var freebusy_data = {};
var freebusy_needsupdate; var freebusy_needsupdate;
@ -628,9 +628,15 @@ function rcube_calendar_ui(settings)
endtime.val("23:59").hide(); endtime.val("23:59").hide();
} }
// read attendee roles from drop-downs
$('select.edit-attendee-role').each(function(i, elem){
if (event_attendees[i])
event_attendees[i].role = $(elem).val();
});
// render time slots // render time slots
var now = new Date(), fb_start = new Date(), fb_end = new Date(); var now = new Date(), fb_start = new Date(), fb_end = new Date();
fb_start.setTime(Math.max(now, event.start)); fb_start.setTime(event.start);
fb_start.setHours(0); fb_start.setMinutes(0); fb_start.setSeconds(0); fb_start.setMilliseconds(0); fb_start.setHours(0); fb_start.setMinutes(0); fb_start.setSeconds(0); fb_start.setMilliseconds(0);
fb_end.setTime(fb_start.getTime() + 86400000); fb_end.setTime(fb_start.getTime() + 86400000);
@ -653,9 +659,23 @@ function rcube_calendar_ui(settings)
$('#schedule-attendees-list').html(list_html); $('#schedule-attendees-list').html(list_html);
// enable/disable buttons
$('#shedule-find-prev').button('option', 'disabled', (fb_start.getTime() < now.getTime()));
// dialog buttons // dialog buttons
var buttons = {}; var buttons = {};
buttons[rcmail.gettext('adobt', 'calendar')] = function() {
$('#edit-startdate').val(startdate.val());
$('#edit-starttime').val(starttime.val());
$('#edit-enddate').val(enddate.val());
$('#edit-endtime').val(endtime.val());
if (freebusy_needsupdate)
update_freebusy_status(me.selected_event);
freebusy_needsupdate = false;
$dialog.dialog("close");
};
buttons[rcmail.gettext('cancel', 'calendar')] = function() { buttons[rcmail.gettext('cancel', 'calendar')] = function() {
$dialog.dialog("close"); $dialog.dialog("close");
}; };
@ -666,6 +686,8 @@ function rcube_calendar_ui(settings)
closeOnEscape: true, closeOnEscape: true,
title: rcmail.gettext('scheduletime', 'calendar'), title: rcmail.gettext('scheduletime', 'calendar'),
close: function() { close: function() {
if (bw.ie6)
$("#edit-attendees-table").css('visibility','visible');
$dialog.dialog("destroy").hide(); $dialog.dialog("destroy").hide();
}, },
buttons: buttons, buttons: buttons,
@ -673,6 +695,10 @@ function rcube_calendar_ui(settings)
width: 850 width: 850
}).show(); }).show();
// hide edit dialog on IE6 because of drop-down elements
if (bw.ie6)
$("#edit-attendees-table").css('visibility','hidden');
// adjust dialog size to fit grid without scrolling // adjust dialog size to fit grid without scrolling
var gridw = $('#schedule-freebusy-times').width(); var gridw = $('#schedule-freebusy-times').width();
var overflow = gridw - $('#attendees-freebusy-table td.times').width() + 1; var overflow = gridw - $('#attendees-freebusy-table td.times').width() + 1;
@ -704,8 +730,8 @@ function rcube_calendar_ui(settings)
lastdate = datestr; lastdate = datestr;
} }
// TODO: define working hours by config // set css class according to working hours
css = (freebusy_ui.numdays == 1 && (curdate.getHours() < 6 || curdate.getHours() > 18)) ? 'offhours' : 'workinghours'; css = (freebusy_ui.numdays == 1 && (curdate.getHours() < settings['work_start'] || curdate.getHours() > settings['work_end'])) ? 'offhours' : 'workinghours';
times_row += '<td class="' + css + '">' + Q($.fullCalendar.formatDate(curdate, settings['time_format'])) + '</td>'; times_row += '<td class="' + css + '">' + Q($.fullCalendar.formatDate(curdate, settings['time_format'])) + '</td>';
slots_row += '<td class="' + css + ' unknown">&nbsp;</td>'; slots_row += '<td class="' + css + ' unknown">&nbsp;</td>';
@ -754,10 +780,10 @@ function rcube_calendar_ui(settings)
else { else {
var table = $('#schedule-freebusy-times'), var table = $('#schedule-freebusy-times'),
width = 0, width = 0,
pos = { top:table.children('thead').height(), left:0 }, pos = { top:table.children('thead').height(), left:-1 },
eventstart = Math.floor(me.selected_event.start.getTime() / 1000), eventstart = date2unixtime(me.selected_event.start),
eventend = Math.floor(me.selected_event.end.getTime() / 1000), eventend = date2unixtime(me.selected_event.end),
slotstart = Math.floor(freebusy_ui.start.getTime() / 1000), slotstart = date2unixtime(freebusy_ui.start),
slotsize = freebusy_ui.interval * 60, slotsize = freebusy_ui.interval * 60,
slotend, fraction, $cell; slotend, fraction, $cell;
@ -782,7 +808,7 @@ function rcube_calendar_ui(settings)
width = table.width() - pos.left; width = table.width() - pos.left;
// overlay is visible // overlay is visible
if (width > 0) if (width > 0 && pos.left >= 0)
overlay.css({ width: (width-5)+'px', height:(table.children('tbody').height() - 4)+'px', left:pos.left+'px', top:pos.top+'px' }).show(); overlay.css({ width: (width-5)+'px', height:(table.children('tbody').height() - 4)+'px', left:pos.left+'px', top:pos.top+'px' }).show();
else else
overlay.hide(); overlay.hide();
@ -852,6 +878,97 @@ function rcube_calendar_ui(settings)
} }
}; };
// attempt to find a time slot where all attemdees are available
var freebusy_find_slot = function(dir)
{
var event = me.selected_event,
eventstart = date2unixtime(event.start), // calculate with unitimes
eventend = date2unixtime(event.end),
duration = eventend - eventstart,
sinterval = freebusy_data.interval * 60,
numslots = Math.ceil(duration / sinterval),
checkdate, slotend, email, curdate;
// shift event times to next possible slot
eventstart += sinterval * dir;
eventend += sinterval * dir;
var candidatecount = 0, candidatestart = candidateend = success = false;
for (var slot = dir > 0 ? freebusy_data.start : freebusy_data.end - sinterval; (dir > 0 && slot < freebusy_data.end) || (dir < 0 && slot >= freebusy_data.start); slot += sinterval * dir) {
slotend = slot + sinterval;
if ((dir > 0 && slotend <= eventstart) || (dir < 0 && slot >= eventend)) // skip
continue;
// respect workingours setting
if (freebusy_ui.workinhoursonly && freebusy_data.interval <= 60) {
curdate = fromunixtime(dir > 0 || !candidateend ? slot : (candidateend - duration));
if (curdate.getHours() < settings['work_start'] || curdate.getHours() > settings['work_end']) { // skip off-hours
candidatestart = candidateend = false;
candidatecount = 0;
continue;
}
}
if (!candidatestart)
candidatestart = slot;
// check freebusy data for all attendees
for (var i=0; i < event_attendees.length; i++) {
if ((email = event_attendees[i].email) && freebusy_data[email][slot] > 1) {
candidatestart = false;
candidatecount = 0;
break;
}
}
// occupied slot
if (!candidatestart)
continue;
// set candidate end to slot end time
candidatecount++;
if (dir < 0 && !candidateend)
candidateend = slotend;
// if candidate is big enough, this is it!
if (candidatecount == numslots) {
if (dir > 0) {
event.start = fromunixtime(candidatestart);
event.end = fromunixtime(candidatestart + duration);
}
else {
event.end = fromunixtime(candidateend);
event.start = fromunixtime(candidateend - duration);
}
success = true;
break;
}
}
// update event date/time fields
if (success) {
$('#schedule-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format']));
$('#schedule-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format']));
$('#schedule-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format']));
$('#schedule-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format']));
freebusy_needsupdate = true;
// move freebusy grid if necessary
if (event.start.getTime() >= freebusy_ui.end.getTime())
render_freebusy_grid(1);
else if (event.end.getTime() <= freebusy_ui.start.getTime())
render_freebusy_grid(-1);
else
render_freebusy_overlay();
var now = new Date();
$('#shedule-find-prev').button('option', 'disabled', (event.start.getTime() < now.getTime()));
}
else {
alert(rcmail.gettext('noslotfound','calendar'));
}
};
// 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()
@ -1701,7 +1818,11 @@ function rcube_calendar_ui(settings)
$('#shedule-freebusy-prev').html(bw.ie6 ? '&lt;&lt;' : '&#9668;').button().click(function(){ render_freebusy_grid(-1); }); $('#shedule-freebusy-prev').html(bw.ie6 ? '&lt;&lt;' : '&#9668;').button().click(function(){ render_freebusy_grid(-1); });
$('#shedule-freebusy-next').html(bw.ie6 ? '&gt;&gt;' : '&#9658;').button().click(function(){ render_freebusy_grid(1); }).parent().buttonset(); $('#shedule-freebusy-next').html(bw.ie6 ? '&gt;&gt;' : '&#9658;').button().click(function(){ render_freebusy_grid(1); }).parent().buttonset();
$('#shedule-find-prev').button().click(function(){ freebusy_find_slot(-1); });
$('#shedule-find-next').button().click(function(){ freebusy_find_slot(1); });
$('#schedule-freebusy-wokinghours').click(function(){ $('#schedule-freebusy-wokinghours').click(function(){
freebusy_ui.workinhoursonly = this.checked;
$('#workinghourscss').remove(); $('#workinghourscss').remove();
if (this.checked) if (this.checked)
$('<style type="text/css" id="workinghourscss"> td.offhours { opacity:0.3; filter:alpha(opacity=30) } </style>').appendTo('head'); $('<style type="text/css" id="workinghourscss"> td.offhours { opacity:0.3; filter:alpha(opacity=30) } </style>').appendTo('head');

View file

@ -52,6 +52,12 @@ $rcmail_config['calendar_first_day'] = 1;
// first hour of the calendar (0-23) // first hour of the calendar (0-23)
$rcmail_config['calendar_first_hour'] = 6; $rcmail_config['calendar_first_hour'] = 6;
// working hours begin
$rcmail_config['calendar_work_start'] = 6;
// working hours end
$rcmail_config['calendar_work_end'] = 18;
// event categories // event categories
$rcmail_config['calendar_categories'] = array( $rcmail_config['calendar_categories'] = array(
'Personal' => 'c0c0c0', 'Personal' => 'c0c0c0',

View file

@ -591,7 +591,7 @@ class calendar_ui
$table->add('attendees', $table->add('attendees',
html::tag('h3', 'boxtitle', $this->calendar->gettext('tabattendees')) . html::tag('h3', 'boxtitle', $this->calendar->gettext('tabattendees')) .
html::div('timesheader', '&nbsp;') . html::div('timesheader', '&nbsp;') .
html::div(array('id' => 'schedule-attendees-list'), '') html::div(array('id' => 'schedule-attendees-list', 'class' => 'attendees-list'), '')
); );
$table->add('times', $table->add('times',
html::div('scroll', html::div('scroll',

View file

@ -31,6 +31,7 @@ $labels['edit'] = 'Edit';
$labels['save'] = 'Save'; $labels['save'] = 'Save';
$labels['remove'] = 'Remove'; $labels['remove'] = 'Remove';
$labels['cancel'] = 'Cancel'; $labels['cancel'] = 'Cancel';
$labels['adobt'] = 'Adopt changes';
$labels['print'] = 'Print calendars'; $labels['print'] = 'Print calendars';
$labels['title'] = 'Summary'; $labels['title'] = 'Summary';
$labels['description'] = 'Description'; $labels['description'] = 'Description';
@ -100,6 +101,9 @@ $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'; $labels['onlyworkinghours'] = 'Show only working hours';
$labels['prevslot'] = 'Previous Slot';
$labels['nextslot'] = 'Next Slot';
$labels['noslotfound'] = 'Unable to find a free time slot';
// event dialog tabs // event dialog tabs
$labels['tabsummary'] = 'Summary'; $labels['tabsummary'] = 'Summary';

View file

@ -597,6 +597,7 @@ td.topalign {
#edit-attendees-legend { #edit-attendees-legend {
margin-top: 3em; margin-top: 3em;
margin-bottom: 0.5em;
} }
#edit-attendees-legend .legend { #edit-attendees-legend .legend {
@ -667,13 +668,33 @@ td.topalign {
border-color: #ccc; border-color: #ccc;
} }
#schedule-attendees-list div.attendee { .attendees-list .attendee {
padding: 3px 20px 3px 6px; padding: 3px 4px 3px 20px;
background: url('images/attendee-status.gif') 2px -97px no-repeat;
}
.attendees-list div.attendee {
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
} }
#schedule-attendees-list div.loading { .attendees-list span.attendee {
background: url('images/loading-small.gif') top right no-repeat; margin-right: 2em;
}
.attendees-list .organizer {
background-position: 3px -77px;
}
.attendees-list .opt-participant {
background-position: 2px -117px;
}
.attendees-list .chair {
background-position: 2px -137px;
}
.attendees-list .loading {
background: url('images/loading-small.gif') 1px 50% no-repeat;
} }
#schedule-freebusy-times { #schedule-freebusy-times {
@ -719,6 +740,15 @@ td.topalign {
right: 0; right: 0;
} }
#eventfreebusy .schedule-find-buttons {
padding-top:1em;
}
#eventfreebusy .schedule-find-buttons button {
min-width: 9em;
text-align: center;
}
span.edit-alarm-set { span.edit-alarm-set {
white-space: nowrap; white-space: nowrap;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -190,11 +190,11 @@
<div class="schedule-options"> <div class="schedule-options">
<label><input type="checkbox" id="schedule-freebusy-wokinghours" value="1" /><roundcube:label name="calendar.onlyworkinghours" /></label> <label><input type="checkbox" id="schedule-freebusy-wokinghours" value="1" /><roundcube:label name="calendar.onlyworkinghours" /></label>
<div class="schedule-buttons"> <div class="schedule-buttons">
<button id="shedule-freebusy-prev" title="<roundcube:label name='previouspage' />">&#9668;</button> <button id="shedule-freebusy-prev" title="<roundcube:label name='previouspage' />">&#9668;</button><button id="shedule-freebusy-next" title="<roundcube:label name='nextpage' />">&#9658;</button>
<button id="shedule-freebusy-next" title="<roundcube:label name='nextpage' />">&#9658;</button>
</div> </div>
</div> </div>
<div style="float:left; width:30em">
<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;
@ -205,8 +205,20 @@
<input type="text" name="enddate" size="10" id="schedule-enddate" disabled="true" /> &nbsp; <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" /> <input type="text" name="endtime" size="6" id="schedule-endtime" disabled="true" />
</div> </div>
</div>
<div class="schedule-find-buttons" style="float:left">
<button id="shedule-find-prev">&#9668; <roundcube:label name="calendar.prevslot" /></button>
<button id="shedule-find-next"><roundcube:label name="calendar.nextslot" /> &#9658;</button>
</div>
<br style="clear:both;" />
<roundcube:include file="/templates/freebusylegend.html" /> <roundcube:include file="/templates/freebusylegend.html" />
<div class="attendees-list">
<span class="attendee organizer"><roundcube:label name="calendar.roleorganizer" /></span>
<span class="attendee req-participant"><roundcube:label name="calendar.rolerequired" /></span>
<span class="attendee opt-participant"><roundcube:label name="calendar.roleoptional" /></span>
<span class="attendee chair"><roundcube:label name="calendar.roleresource" /></span>
</div>
</div> </div>
<div id="calendarform" class="uidialog"> <div id="calendarform" class="uidialog">