Restore next/previous slot suggestion for events
parent
aaf4166c44
commit
540e81b670
2
NEWS
2
NEWS
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
Enhancements
|
Enhancements
|
||||||
- [web] create card from sender or recipient address (#3002, #4610)
|
- [web] create card from sender or recipient address (#3002, #4610)
|
||||||
|
- [web] updated Angular to version 1.7.7
|
||||||
|
- [web] restored support for next/previous slot suggestion in attendees editor
|
||||||
- [core] baseDN now accept dynamic domain values (#3685 - sponsored by iRedMail)
|
- [core] baseDN now accept dynamic domain values (#3685 - sponsored by iRedMail)
|
||||||
- [core] we now handle optional and non-required attendee states
|
- [core] we now handle optional and non-required attendee states
|
||||||
|
|
||||||
|
|
|
@ -130,6 +130,7 @@
|
||||||
<div layout="row">
|
<div layout="row">
|
||||||
<md-checkbox flex="50"
|
<md-checkbox flex="50"
|
||||||
ng-model="editor.component.isAllDay"
|
ng-model="editor.component.isAllDay"
|
||||||
|
ng-change="editor.updateFreeBusyCoverage()"
|
||||||
ng-true-value="1"
|
ng-true-value="1"
|
||||||
ng-false-value="0"
|
ng-false-value="0"
|
||||||
label:aria-label="All day Event">
|
label:aria-label="All day Event">
|
||||||
|
@ -295,7 +296,7 @@
|
||||||
</div>
|
</div>
|
||||||
</md-dialog-content>
|
</md-dialog-content>
|
||||||
<!-- cancel/reset/save -->
|
<!-- cancel/reset/save -->
|
||||||
<md-dialog-actions ng-hide="editor.attendeeConflictError">
|
<md-dialog-actions class="ng-hide" ng-hide="editor.attendeeConflictError">
|
||||||
<md-button type="button" ng-click="editor.cancel(eventForm)">
|
<md-button type="button" ng-click="editor.cancel(eventForm)">
|
||||||
<var:string label:value="Cancel"/>
|
<var:string label:value="Cancel"/>
|
||||||
</md-button>
|
</md-button>
|
||||||
|
@ -336,7 +337,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</md-dialog-content>
|
</md-dialog-content>
|
||||||
<md-dialog-actions ng-show="editor.attendeeConflictError.conflicts">
|
<md-dialog-actions class="ng-hide" ng-show="editor.attendeeConflictError.conflicts">
|
||||||
<md-button type="button" ng-click="editor.cancel(eventForm)">
|
<md-button type="button" ng-click="editor.cancel(eventForm)">
|
||||||
<var:string label:value="Cancel"/>
|
<var:string label:value="Cancel"/>
|
||||||
</md-button>
|
</md-button>
|
||||||
|
|
|
@ -10,7 +10,23 @@
|
||||||
var dayEndHour = <var:string value="dayEndHour"/>;
|
var dayEndHour = <var:string value="dayEndHour"/>;
|
||||||
var timeFormat = '<var:string value="userDefaults.timeFormat" const:escapeHTML="NO"/>';
|
var timeFormat = '<var:string value="userDefaults.timeFormat" const:escapeHTML="NO"/>';
|
||||||
</script>
|
</script>
|
||||||
<md-content>
|
<div layout="row" layout-align="end center">
|
||||||
|
<!-- suggestions options -->
|
||||||
|
<md-checkbox ng-model="editor.component.$attendees.workDaysOnly">
|
||||||
|
<var:string label:value="Work days only"/>
|
||||||
|
</md-checkbox>
|
||||||
|
<sg-timepicker ng-model="editor.component.$attendees.slotStartTimeLimit"><!-- slot start --></sg-timepicker>
|
||||||
|
<sg-timepicker ng-model="editor.component.$attendees.slotEndTimeLimit"><!-- slot end --></sg-timepicker>
|
||||||
|
<md-button class="md-icon-button"
|
||||||
|
label:aria-label="Previous slot"
|
||||||
|
ng-click="editor.previousSlot()"
|
||||||
|
md-no-ink="md-no-ink"><md-icon>chevron_left</md-icon></md-button>
|
||||||
|
<md-button class="md-icon-button"
|
||||||
|
label:aria-label="Next slot"
|
||||||
|
ng-click="editor.nextSlot()"
|
||||||
|
md-no-ink="md-no-ink"><md-icon>chevron_right</md-icon></md-button>
|
||||||
|
</div>
|
||||||
|
<div layout="row">
|
||||||
<!-- attendees -->
|
<!-- attendees -->
|
||||||
<md-list>
|
<md-list>
|
||||||
<md-list-item>
|
<md-list-item>
|
||||||
|
@ -24,7 +40,7 @@
|
||||||
<sg-avatar-image class="md-avatar"
|
<sg-avatar-image class="md-avatar"
|
||||||
sg-email="editor.component.organizer.email"
|
sg-email="editor.component.organizer.email"
|
||||||
size="40">person</sg-avatar-image>
|
size="40">person</sg-avatar-image>
|
||||||
<div class="sg-tile-content">
|
<div class="sg-tile-content sg-padded--right">
|
||||||
<div class="sg-md-subhead"><div>{{editor.component.organizer.name}}</div></div>
|
<div class="sg-md-subhead"><div>{{editor.component.organizer.name}}</div></div>
|
||||||
<div class="sg-md-body"><div>{{editor.component.organizer.email}}</div></div>
|
<div class="sg-md-body"><div>{{editor.component.organizer.email}}</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,69 +56,72 @@
|
||||||
<sg-avatar-image class="md-avatar"
|
<sg-avatar-image class="md-avatar"
|
||||||
sg-email="currentAttendee.email"
|
sg-email="currentAttendee.email"
|
||||||
size="40">person</sg-avatar-image>
|
size="40">person</sg-avatar-image>
|
||||||
<div class="sg-tile-content">
|
<div class="sg-tile-content sg-padded--right">
|
||||||
<div class="sg-md-subhead"><div>{{currentAttendee.name}}</div></div>
|
<div class="sg-md-subhead"><div>{{currentAttendee.name}}</div></div>
|
||||||
<div class="sg-md-body"><div>{{currentAttendee.email}}</div></div>
|
<div class="sg-md-body"><div>{{currentAttendee.email}}</div></div>
|
||||||
</div>
|
</div>
|
||||||
<md-divider><!-- divider --></md-divider>
|
<md-divider><!-- divider --></md-divider>
|
||||||
</md-list-item>
|
</md-list-item>
|
||||||
</md-list>
|
</md-list>
|
||||||
<!-- freebusy -->
|
<md-content id="freebusy">
|
||||||
<md-list class="day"
|
<!-- freebusy -->
|
||||||
ng-repeat="day in editor.attendeesEditor.days">
|
<md-list class="day"
|
||||||
<!-- timeline -->
|
id="freebusy_day_{{ day.getDayString }}"
|
||||||
<md-list-item layout-align="start end">
|
ng-repeat="day in editor.attendeesEditor.days track by day.getDayString">
|
||||||
<div layout="column" layout-align="end start">
|
<!-- timeline -->
|
||||||
<div>{{day.stringWithSeparator}}</div>
|
<md-list-item layout-align="start end">
|
||||||
<div class="hours" layout="row" layout-align="space-between center">
|
<div layout="column" layout-align="end start">
|
||||||
<div class="hour" ng-repeat="hour in ::editor.attendeesEditor.hours">{{hour}}</div>
|
<div>{{day.stringWithSeparator}}</div>
|
||||||
|
<div class="hours" layout="row" layout-align="space-between center">
|
||||||
|
<div class="hour" ng-repeat="hour in ::editor.attendeesEditor.hours">{{hour}}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</md-list-item>
|
||||||
</md-list-item>
|
<!-- organizer freebusy -->
|
||||||
<!-- organizer freebusy -->
|
<md-list-item>
|
||||||
<md-list-item>
|
<div class="hour"
|
||||||
<div class="hour"
|
ng-class="{'sg-no-freebusy': !editor.component.organizer.uid}"
|
||||||
ng-class="{'sg-no-freebusy': !editor.component.organizer.uid}"
|
ng-repeat="hour in ::editor.attendeesEditor.hours">
|
||||||
ng-repeat="hour in ::editor.attendeesEditor.hours">
|
<div class="quarter" ng-class="{event: editor.coversFreeBusy(day.getDayString, hour, 0)}">
|
||||||
<div class="quarter" ng-class="{event: editor.component.coversFreeBusy(day.getDayString, hour, 0)}">
|
<div class="busy" ng-show="editor.component.organizer.freebusy[day.getDayString][hour][0]"><!-- 15 minutes --></div>
|
||||||
<div class="busy" ng-show="editor.component.organizer.freebusy[day.getDayString][hour][0]"><!-- 15 minutes --></div>
|
</div>
|
||||||
|
<div class="quarter" ng-class="{event: editor.coversFreeBusy(day.getDayString, hour, 1)}">
|
||||||
|
<div class="busy" ng-show="editor.component.organizer.freebusy[day.getDayString][hour][1]"><!-- 15 minutes --></div>
|
||||||
|
</div>
|
||||||
|
<div class="quarter" ng-class="{event: editor.coversFreeBusy(day.getDayString, hour, 2)}">
|
||||||
|
<div class="busy" ng-show="editor.component.organizer.freebusy[day.getDayString][hour][2]"><!-- 15 minutes --></div>
|
||||||
|
</div>
|
||||||
|
<div class="quarter" ng-class="{event: editor.coversFreeBusy(day.getDayString, hour, 3)}">
|
||||||
|
<div class="busy" ng-show="editor.component.organizer.freebusy[day.getDayString][hour][3]"><!-- 15 minutes --></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="quarter" ng-class="{event: editor.component.coversFreeBusy(day.getDayString, hour, 1)}">
|
<md-divider><!-- divider --></md-divider>
|
||||||
<div class="busy" ng-show="editor.component.organizer.freebusy[day.getDayString][hour][1]"><!-- 15 minutes --></div>
|
</md-list-item>
|
||||||
|
<!-- attendees freebusy -->
|
||||||
|
<md-list-item ng-repeat="currentAttendee in editor.component.attendees track by currentAttendee.email">
|
||||||
|
<div class="hour"
|
||||||
|
ng-class="{'sg-no-freebusy': !currentAttendee.uid}"
|
||||||
|
ng-repeat="hour in ::editor.attendeesEditor.hours">
|
||||||
|
<div class="quarter" ng-class="{event: editor.coversFreeBusy(day.getDayString, hour, 0)}">
|
||||||
|
<div class="busy" ng-show="currentAttendee.freebusy[day.getDayString][hour][0]"><!-- 15 minutes --></div>
|
||||||
|
</div>
|
||||||
|
<div class="quarter" ng-class="{event: editor.coversFreeBusy(day.getDayString, hour, 1)}">
|
||||||
|
<div class="busy" ng-show="currentAttendee.freebusy[day.getDayString][hour][1]"><!-- 15 minutes --></div>
|
||||||
|
</div>
|
||||||
|
<div class="quarter" ng-class="{event: editor.coversFreeBusy(day.getDayString, hour, 2)}">
|
||||||
|
<div class="busy" ng-show="currentAttendee.freebusy[day.getDayString][hour][2]"><!-- 15 minutes --></div>
|
||||||
|
</div>
|
||||||
|
<div class="quarter" ng-class="{event: editor.coversFreeBusy(day.getDayString, hour, 3)}">
|
||||||
|
<div class="busy" ng-show="currentAttendee.freebusy[day.getDayString][hour][3]"><!-- 15 minutes --></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="quarter" ng-class="{event: editor.component.coversFreeBusy(day.getDayString, hour, 2)}">
|
<md-divider><!-- divider --></md-divider>
|
||||||
<div class="busy" ng-show="editor.component.organizer.freebusy[day.getDayString][hour][2]"><!-- 15 minutes --></div>
|
</md-list-item>
|
||||||
</div>
|
</md-list>
|
||||||
<div class="quarter" ng-class="{event: editor.component.coversFreeBusy(day.getDayString, hour, 3)}">
|
</md-content>
|
||||||
<div class="busy" ng-show="editor.component.organizer.freebusy[day.getDayString][hour][3]"><!-- 15 minutes --></div>
|
</div><!-- row -->
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<md-divider><!-- divider --></md-divider>
|
|
||||||
</md-list-item>
|
|
||||||
<!-- attendees freebusy -->
|
|
||||||
<md-list-item ng-repeat="currentAttendee in editor.component.attendees track by currentAttendee.email">
|
|
||||||
<div class="hour"
|
|
||||||
ng-class="{'sg-no-freebusy': !currentAttendee.uid}"
|
|
||||||
ng-repeat="hour in ::editor.attendeesEditor.hours">
|
|
||||||
<div class="quarter" ng-class="{event: editor.component.coversFreeBusy(day.getDayString, hour, 0)}">
|
|
||||||
<div class="busy" ng-show="currentAttendee.freebusy[day.getDayString][hour][0]"><!-- 15 minutes --></div>
|
|
||||||
</div>
|
|
||||||
<div class="quarter" ng-class="{event: editor.component.coversFreeBusy(day.getDayString, hour, 1)}">
|
|
||||||
<div class="busy" ng-show="currentAttendee.freebusy[day.getDayString][hour][1]"><!-- 15 minutes --></div>
|
|
||||||
</div>
|
|
||||||
<div class="quarter" ng-class="{event: editor.component.coversFreeBusy(day.getDayString, hour, 2)}">
|
|
||||||
<div class="busy" ng-show="currentAttendee.freebusy[day.getDayString][hour][2]"><!-- 15 minutes --></div>
|
|
||||||
</div>
|
|
||||||
<div class="quarter" ng-class="{event: editor.component.coversFreeBusy(day.getDayString, hour, 3)}">
|
|
||||||
<div class="busy" ng-show="currentAttendee.freebusy[day.getDayString][hour][3]"><!-- 15 minutes --></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<md-divider><!-- divider --></md-divider>
|
|
||||||
</md-list-item>
|
|
||||||
</md-list>
|
|
||||||
</md-content>
|
|
||||||
<!-- freebusy caption -->
|
<!-- freebusy caption -->
|
||||||
<div layout="row" layout-align="end center">
|
<div layout="row" layout-align="end center" class="sg-padded--top">
|
||||||
<div class="quarter"><div class="busy sg-color-chip"><!-- busy --></div></div>
|
<div class="quarter"><div class="busy sg-color-chip"><!-- busy --></div></div>
|
||||||
<label class="md-caption"><var:string label:value="Busy"/></label>
|
<label class="md-caption"><var:string label:value="Busy"/></label>
|
||||||
<div class="quarter"><div class="sg-no-freebusy sg-color-sample"><!-- no fb --></div></div>
|
<div class="quarter"><div class="sg-no-freebusy sg-color-sample"><!-- no fb --></div></div>
|
||||||
|
|
|
@ -260,6 +260,14 @@ String.prototype.parseDate = function(localeProvider, format) {
|
||||||
return new Date(NaN);
|
return new Date(NaN);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Date.prototype.clone = function() {
|
||||||
|
var newDate = new Date();
|
||||||
|
|
||||||
|
newDate.setTime(this.getTime());
|
||||||
|
|
||||||
|
return newDate;
|
||||||
|
};
|
||||||
|
|
||||||
Date.prototype.daysUpTo = function(otherDate) {
|
Date.prototype.daysUpTo = function(otherDate) {
|
||||||
var days = [];
|
var days = [];
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,647 @@
|
||||||
|
/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Attendees
|
||||||
|
* @constructor
|
||||||
|
* @param {object} component - a Component object instance
|
||||||
|
*/
|
||||||
|
function Attendees(component) {
|
||||||
|
this.component = component;
|
||||||
|
if (this.component.attendees) {
|
||||||
|
_.forEach(this.component.attendees, function(attendee) {
|
||||||
|
attendee.image = Attendees.$gravatar(attendee.email, 32);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.workDaysOnly = true;
|
||||||
|
this.slotStartTimeLimit = new Date();
|
||||||
|
this.slotStartTimeLimit.setMinutes(0);
|
||||||
|
this.slotStartTimeLimit.setHours(Attendees.dayStartHour);
|
||||||
|
this.slotEndTimeLimit = new Date();
|
||||||
|
this.slotEndTimeLimit.setMinutes(0);
|
||||||
|
this.slotEndTimeLimit.setHours(Attendees.dayEndHour);
|
||||||
|
this.$days = [];
|
||||||
|
this.updateFreeBusyCoverage();
|
||||||
|
this.updateFreeBusy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @memberof Attendees
|
||||||
|
* @desc The factory we'll use to register with Angular
|
||||||
|
* @returns the Attendees constructor
|
||||||
|
*/
|
||||||
|
Attendees.$factory = ['$q', '$timeout', '$log', 'sgSettings', 'Preferences', 'User', 'Card', 'Gravatar', 'Resource', function($q, $timeout, $log, Settings, Preferences, User, Card, Gravatar, Resource) {
|
||||||
|
angular.extend(Attendees, {
|
||||||
|
$q: $q,
|
||||||
|
$timeout: $timeout,
|
||||||
|
$log: $log,
|
||||||
|
$settings: Settings,
|
||||||
|
$User: User,
|
||||||
|
$Preferences: Preferences,
|
||||||
|
$Card: Card,
|
||||||
|
$gravatar: Gravatar,
|
||||||
|
$$resource: new Resource(Settings.activeUser('folderURL') + 'Calendar', Settings.activeUser())
|
||||||
|
});
|
||||||
|
|
||||||
|
Attendees.dayStartHour = parseInt(Preferences.defaults.SOGoDayStartTime.split(':')[0]);
|
||||||
|
Attendees.dayEndHour = parseInt(Preferences.defaults.SOGoDayEndTime.split(':')[0]);
|
||||||
|
|
||||||
|
return Attendees; // return constructor
|
||||||
|
}];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module SOGo.SchedulerUI
|
||||||
|
* @desc Factory registration of Attendees in Angular module.
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
angular.module('SOGo.SchedulerUI');
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
angular.module('SOGo.SchedulerUI', ['SOGo.Common']);
|
||||||
|
}
|
||||||
|
angular.module('SOGo.SchedulerUI')
|
||||||
|
.factory('Attendees', Attendees.$factory);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function timeToQuarters
|
||||||
|
* @memberof Attendees
|
||||||
|
* @param {date} dateTime - a Date object instance
|
||||||
|
* @desc Return the number of quarters matching the time
|
||||||
|
* @returns the number of quarters
|
||||||
|
*/
|
||||||
|
Attendees.timeToQuarters = function(dateTime) {
|
||||||
|
return dateTime.getHours() * 4 + Math.ceil(dateTime.getMinutes()/15);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function getLength
|
||||||
|
* @memberof Attendees.prototype
|
||||||
|
* @returns the number of attendees
|
||||||
|
*/
|
||||||
|
Attendees.prototype.getLength = function() {
|
||||||
|
return this.component.attendees ? this.component.attendees.length : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function initOrganizer
|
||||||
|
* @memberof Attendees.prototype
|
||||||
|
* @desc Extend instance with organizer including her freebusy information.
|
||||||
|
* @param {object} calendar - Calendar instance associated to current component
|
||||||
|
*/
|
||||||
|
Attendees.prototype.initOrganizer = function(calendar) {
|
||||||
|
var _this = this, promise;
|
||||||
|
if (calendar && calendar.isSubscription) {
|
||||||
|
promise = Attendees.$User.$filter(calendar.owner).then(function(results) {
|
||||||
|
var owner = results[0];
|
||||||
|
_this.component.organizer = {
|
||||||
|
uid: owner.uid,
|
||||||
|
name: owner.cn,
|
||||||
|
email: owner.c_email
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.component.organizer = {
|
||||||
|
uid: Attendees.$settings.activeUser('login'),
|
||||||
|
name: Attendees.$settings.activeUser('identification'),
|
||||||
|
email: Attendees.$settings.activeUser('email')
|
||||||
|
};
|
||||||
|
promise = Attendees.$q.when();
|
||||||
|
}
|
||||||
|
// Fetch organizer's freebusy
|
||||||
|
promise.then(function() {
|
||||||
|
_this.updateFreeBusyAttendee(_this.component.organizer);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function add
|
||||||
|
* @memberof Attendees.prototype
|
||||||
|
* @desc Add an attendee and fetch his freebusy info.
|
||||||
|
* @param {Object} card - an Card object instance to be added to the attendees list
|
||||||
|
*/
|
||||||
|
Attendees.prototype.add = function(card, options) {
|
||||||
|
var _this = this, attendee, list, url, params;
|
||||||
|
if (card) {
|
||||||
|
if (!this.component.attendees || (options && options.organizerCalendar)) {
|
||||||
|
// No attendee yet; initialize the organizer
|
||||||
|
this.initOrganizer(options? options.organizerCalendar : undefined);
|
||||||
|
}
|
||||||
|
if (card.$isList({expandable: true})) {
|
||||||
|
// Decompose list members
|
||||||
|
list = Attendees.$Card.$find(card.container, card.c_name);
|
||||||
|
list.$id().then(function(listId) {
|
||||||
|
_.forEach(list.refs, function(ref) {
|
||||||
|
attendee = {
|
||||||
|
name: ref.c_cn,
|
||||||
|
email: ref.$preferredEmail(options? options.partial : undefined),
|
||||||
|
role: 'req-participant',
|
||||||
|
partstat: 'needs-action',
|
||||||
|
uid: ref.c_uid,
|
||||||
|
$avatarIcon: 'person',
|
||||||
|
};
|
||||||
|
if (!_.find(_this.component.attendees, function(o) {
|
||||||
|
return o.email == attendee.email;
|
||||||
|
})) {
|
||||||
|
// Contact is not already an attendee, add it
|
||||||
|
attendee.image = Attendees.$gravatar(attendee.email, 32);
|
||||||
|
if (_this.component.attendees)
|
||||||
|
_this.component.attendees.push(attendee);
|
||||||
|
else
|
||||||
|
_this.component.attendees = [attendee];
|
||||||
|
_this.updateFreeBusyAttendee(attendee);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Single contact
|
||||||
|
attendee = {
|
||||||
|
uid: card.c_uid,
|
||||||
|
domain: card.c_domain,
|
||||||
|
isMSExchange: card.ismsexchange,
|
||||||
|
name: card.c_cn,
|
||||||
|
email: card.$preferredEmail(),
|
||||||
|
role: 'req-participant',
|
||||||
|
partstat: 'needs-action',
|
||||||
|
$avatarIcon: card.$avatarIcon
|
||||||
|
};
|
||||||
|
if (!_.find(this.attendees, function(o) {
|
||||||
|
return o.email == attendee.email;
|
||||||
|
})) {
|
||||||
|
attendee.image = Attendees.$gravatar(attendee.email, 32);
|
||||||
|
if (this.component.attendees)
|
||||||
|
this.component.attendees.push(attendee);
|
||||||
|
else
|
||||||
|
this.component.attendees = [attendee];
|
||||||
|
this.updateFreeBusyAttendee(attendee);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function hasAttendee
|
||||||
|
* @memberof Attendees.prototype
|
||||||
|
* @desc Verify if one of the email addresses of a Card instance matches an attendee.
|
||||||
|
* @param {Object} card - an Card object instance
|
||||||
|
* @returns true if the Card matches an attendee
|
||||||
|
*/
|
||||||
|
Attendees.prototype.hasAttendee = function(card) {
|
||||||
|
var attendee = _.find(this.component.attendees, function(attendee) {
|
||||||
|
return _.find(card.emails, function(email) {
|
||||||
|
return email.value == attendee.email;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return angular.isDefined(attendee);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function remove
|
||||||
|
* @memberof Attendees.prototype
|
||||||
|
* @desc Remove an attendee from the component.
|
||||||
|
* @param {Object} attendee - an object literal defining an attendee
|
||||||
|
*/
|
||||||
|
Attendees.prototype.remove = function(attendee) {
|
||||||
|
var index = _.findIndex(this.component.attendees, function(currentAttendee) {
|
||||||
|
return currentAttendee.email == attendee.email;
|
||||||
|
});
|
||||||
|
this.component.attendees.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function updateFreeBusyCoverage
|
||||||
|
* @memberof Attendees.prototype
|
||||||
|
* @desc Build a 15-minute-based representation of the component's period.
|
||||||
|
* @returns an object literal hashed by days and hours and arrays of four 1's and 0's
|
||||||
|
*/
|
||||||
|
Attendees.prototype.updateFreeBusyCoverage = function() {
|
||||||
|
var _this = this, freebusy = {};
|
||||||
|
var roundedStart, roundedEnd, startQuarter, endQuarter;
|
||||||
|
|
||||||
|
if (this.component.start && this.component.end) {
|
||||||
|
roundedStart = new Date(this.component.start.getTime());
|
||||||
|
roundedEnd = new Date(this.component.end.getTime());
|
||||||
|
if (this.component.isAllDay) {
|
||||||
|
roundedStart.setHours(Attendees.dayStartHour);
|
||||||
|
roundedStart.setMinutes(0);
|
||||||
|
roundedEnd.setHours(Attendees.dayEndHour);
|
||||||
|
roundedEnd.setMinutes(0);
|
||||||
|
startQuarter = endQuarter = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
startQuarter = parseInt(roundedStart.getMinutes()/15 + 0.5);
|
||||||
|
endQuarter = parseInt(roundedEnd.getMinutes()/15 + 0.5);
|
||||||
|
}
|
||||||
|
roundedStart.setMinutes(15*startQuarter);
|
||||||
|
roundedEnd.setMinutes(15*endQuarter);
|
||||||
|
|
||||||
|
_.forEach(roundedStart.daysUpTo(roundedEnd), function(date, index) {
|
||||||
|
var currentDay = date.getDate(),
|
||||||
|
dayKey = date.getDayString(),
|
||||||
|
hourKey;
|
||||||
|
if (dayKey === roundedStart.getDayString()) {
|
||||||
|
hourKey = date.getHours().toString();
|
||||||
|
freebusy[dayKey] = {};
|
||||||
|
freebusy[dayKey][hourKey] = [];
|
||||||
|
while (startQuarter > 0) {
|
||||||
|
freebusy[dayKey][hourKey].push(0);
|
||||||
|
startQuarter--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
date = date.beginOfDay();
|
||||||
|
freebusy[dayKey] = {};
|
||||||
|
}
|
||||||
|
while (date.getTime() < roundedEnd.getTime() &&
|
||||||
|
date.getDate() == currentDay) {
|
||||||
|
hourKey = date.getHours().toString();
|
||||||
|
if (angular.isUndefined(freebusy[dayKey][hourKey]))
|
||||||
|
freebusy[dayKey][hourKey] = [];
|
||||||
|
freebusy[dayKey][hourKey].push(1);
|
||||||
|
date.addMinutes(15);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.freebusy = freebusy;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function coversFreeBusy
|
||||||
|
* @memberof Attendees.prototype
|
||||||
|
* @desc Check if a specific quarter matches the component's period.
|
||||||
|
* @returns true if the quarter covers the component's period
|
||||||
|
*/
|
||||||
|
Attendees.prototype.coversFreeBusy = function(day, hour, quarter) {
|
||||||
|
var b = (this.freebusy &&
|
||||||
|
angular.isDefined(this.freebusy[day]) &&
|
||||||
|
angular.isDefined(this.freebusy[day][hour]) &&
|
||||||
|
this.freebusy[day][hour][quarter] == 1);
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function getDays
|
||||||
|
* @memberof Attendees.prototype
|
||||||
|
* @desc Define a period of one week before and one week after the component's period or a reference date.
|
||||||
|
* @param refDate - a Date object
|
||||||
|
* @returns an array of objects representing the days
|
||||||
|
*/
|
||||||
|
Attendees.prototype.getDays = function(refDate) {
|
||||||
|
var _this = this, sd, ed, formatFcn;
|
||||||
|
|
||||||
|
if (refDate) {
|
||||||
|
sd = refDate;
|
||||||
|
ed = new Date(refDate.getTime());
|
||||||
|
ed.addMinutes(this.component.delta);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sd = this.component.start;
|
||||||
|
ed = this.component.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$days.length === 0 ||
|
||||||
|
_.findIndex(this.$days, ['getDayString', sd.getDayString()]) < 0 ||
|
||||||
|
_.findIndex(this.$days, ['getDayString', ed.getDayString()]) < 0) {
|
||||||
|
sd = sd.beginOfDay().addDays(-7);
|
||||||
|
ed = ed.beginOfDay().addDays(7);
|
||||||
|
formatFcn = Attendees.$Preferences.$mdDateLocaleProvider.formatDate;
|
||||||
|
this.$days.splice(0, this.$days.length);
|
||||||
|
_.forEach(sd.daysUpTo(ed), function(date) {
|
||||||
|
date.$dateFormat = Attendees.$Preferences.defaults.SOGoLongDateFormat;
|
||||||
|
_this.$days.push({
|
||||||
|
stringWithSeparator: formatFcn(date),
|
||||||
|
getDayString: date.getDayString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.$days;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function updateFreeBusy
|
||||||
|
* @memberof Attendees.prototype
|
||||||
|
* @desc Fetch the freebusy information of the organizer and all attendees.
|
||||||
|
* @returns a promise of the all HTTP operations
|
||||||
|
*/
|
||||||
|
Attendees.prototype.updateFreeBusy = function(refDate) {
|
||||||
|
var _this = this, promises = [];
|
||||||
|
|
||||||
|
if (this.getLength() > 0) {
|
||||||
|
if (this.component.organizer) {
|
||||||
|
promises.push(this.updateFreeBusyAttendee(this.component.organizer, refDate));
|
||||||
|
}
|
||||||
|
_.forEach(_.filter(this.component.attendees, 'uid'), function(attendee) {
|
||||||
|
promises.push(_this.updateFreeBusyAttendee(attendee, refDate));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Attendees.$q.all(promises);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function updateFreeBusyAttendee
|
||||||
|
* @memberof Attendees.prototype
|
||||||
|
* @desc Update the freebusy information for the component's period for a specific attendee.
|
||||||
|
* @param {Object} card - an Card object instance of the attendee
|
||||||
|
* @returns a promise of the HTTP operation if the information was not cached
|
||||||
|
*/
|
||||||
|
Attendees.prototype.updateFreeBusyAttendee = function(attendee, refDate) {
|
||||||
|
var promise, resource, uid, sd, ed, params, days;
|
||||||
|
|
||||||
|
if (attendee.uid) {
|
||||||
|
uid = attendee.uid;
|
||||||
|
if (attendee.domain)
|
||||||
|
uid += '@' + attendee.domain;
|
||||||
|
days = _.map(this.getDays(refDate), 'getDayString');
|
||||||
|
params =
|
||||||
|
{
|
||||||
|
sday: days[0],
|
||||||
|
eday: days[days.length - 1]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (attendee.isMSExchange) {
|
||||||
|
// Attendee is not a local user, but her freebusy data is available from an external MS Exchange server;
|
||||||
|
// we query /SOGo/so/<login_user>/freebusy.ifb/ajaxRead?uid=<uid>
|
||||||
|
resource = Attendees.$$resource.userResource();
|
||||||
|
params.uid = uid;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Attendee is a user;
|
||||||
|
// web query /SOGo/so/<uid>/freebusy.ifb/ajaxRead
|
||||||
|
resource = Attendees.$$resource.userResource(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (angular.isUndefined(attendee.freebusy))
|
||||||
|
attendee.freebusy = {};
|
||||||
|
|
||||||
|
if (_.intersection(_.keys(attendee.freebusy), days).length !== days.length) {
|
||||||
|
// Fetch FreeBusy information
|
||||||
|
promise = resource.fetch('freebusy.ifb', 'ajaxRead', params).then(function(data) {
|
||||||
|
_.forEach(days, function(day) {
|
||||||
|
var hour;
|
||||||
|
|
||||||
|
if (angular.isUndefined(attendee.freebusy[day]))
|
||||||
|
attendee.freebusy[day] = {};
|
||||||
|
|
||||||
|
if (angular.isUndefined(data[day]))
|
||||||
|
data[day] = {};
|
||||||
|
|
||||||
|
for (var i = 0; i <= 23; i++) {
|
||||||
|
hour = i.toString();
|
||||||
|
if (data[day][hour])
|
||||||
|
attendee.freebusy[day][hour] = [
|
||||||
|
data[day][hour]["0"],
|
||||||
|
data[day][hour]["15"],
|
||||||
|
data[day][hour]["30"],
|
||||||
|
data[day][hour]["45"]
|
||||||
|
];
|
||||||
|
else
|
||||||
|
attendee.freebusy[day][hour] = [0, 0, 0, 0];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
promise = Attendees.$q.when();
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function forwardFindDate
|
||||||
|
* @memberof Attendees.prototype
|
||||||
|
* @desc Find the next slot for which all attendees are available whitin the reference day
|
||||||
|
* @param {date} currentStart - the reference day
|
||||||
|
* @returns a date object or null if no slot were found
|
||||||
|
*/
|
||||||
|
Attendees.prototype.forwardFindDate = function(currentStart) {
|
||||||
|
var foundDate = null;
|
||||||
|
var maxOffset = this.endLimit - this.duration;
|
||||||
|
var offset = 0;
|
||||||
|
|
||||||
|
if (this.firstStep) {
|
||||||
|
offset = Math.floor(this.start.getHours() * 4 + this.start.getMinutes() / 15) + 1;
|
||||||
|
this.firstStep = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
offset = this.currentEntries.indexOf(0);
|
||||||
|
}
|
||||||
|
if (offset > -1 && offset < this.startLimit) {
|
||||||
|
offset = this.startLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!foundDate && offset > -1 && offset <= maxOffset) {
|
||||||
|
var testDuration = 0;
|
||||||
|
while (this.currentEntries[offset] === 0 && testDuration < this.duration) {
|
||||||
|
testDuration++;
|
||||||
|
offset++;
|
||||||
|
}
|
||||||
|
if (testDuration == this.duration) {
|
||||||
|
foundDate = new Date();
|
||||||
|
var foundTime = (currentStart.getTime() + (offset - testDuration) * 900000);
|
||||||
|
foundDate.setTime(foundTime);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
offset = this.currentEntries.indexOf(0, offset + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function forwardAdjustCurrentStart
|
||||||
|
* @memberof Attendees.prototype
|
||||||
|
* @desc Adjust a date to the next non-weekend day
|
||||||
|
* @param {date} currentStart - the reference day
|
||||||
|
*/
|
||||||
|
Attendees.prototype.forwardAdjustCurrentStart = function (currentStart) {
|
||||||
|
var day = currentStart.getDay();
|
||||||
|
if (day === 0) {
|
||||||
|
currentStart.addDays(1);
|
||||||
|
}
|
||||||
|
else if (day === 6) {
|
||||||
|
currentStart.addDays(2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function backwardFindDate
|
||||||
|
* @memberof Attendees.prototype
|
||||||
|
* @desc Find the previous slot for which all attendees are available whitin the reference day
|
||||||
|
* @param {date} currentStart - the reference day
|
||||||
|
* @returns a date object or null if no slot were found
|
||||||
|
*/
|
||||||
|
Attendees.prototype.backwardFindDate = function (currentStart) {
|
||||||
|
var foundDate = null;
|
||||||
|
var maxOffset = this.endLimit - this.duration;
|
||||||
|
var offset;
|
||||||
|
if (this.firstStep) {
|
||||||
|
offset = Math.floor(this.start.getHours() * 4 + this.start.getMinutes() / 15) - 1;
|
||||||
|
this.firstStep = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
offset = this.currentEntries.lastIndexOf(0);
|
||||||
|
}
|
||||||
|
if (offset > maxOffset) {
|
||||||
|
offset = maxOffset;
|
||||||
|
}
|
||||||
|
while (!foundDate && offset >= this.startLimit) {
|
||||||
|
var testDuration = 0;
|
||||||
|
var testOffset = offset;
|
||||||
|
while (this.currentEntries[testOffset] === 0 && testDuration < this.duration) {
|
||||||
|
testDuration++;
|
||||||
|
testOffset++;
|
||||||
|
}
|
||||||
|
if (testDuration == this.duration) {
|
||||||
|
foundDate = new Date();
|
||||||
|
var foundTime = (currentStart.getTime() + offset * 900000);
|
||||||
|
foundDate.setTime(foundTime);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
offset = this.currentEntries.lastIndexOf(0, offset - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Attendees.$log.debug(['found = ' + foundDate, offset]);
|
||||||
|
return foundDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function backwardAdjustCurrentStart
|
||||||
|
* @memberof Attendees.prototype
|
||||||
|
* @desc Adjust a date to the previous non-weekend day
|
||||||
|
* @param {date} currentStart - the reference day
|
||||||
|
*/
|
||||||
|
Attendees.prototype.backwardAdjustCurrentStart = function (currentStart) {
|
||||||
|
var day = currentStart.getDay();
|
||||||
|
if (day == 0) {
|
||||||
|
currentStart.addDays(-2);
|
||||||
|
}
|
||||||
|
else if (day == 6) {
|
||||||
|
currentStart.addDays(-1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function findSlot
|
||||||
|
* @memberof Attendees.prototype
|
||||||
|
* @desc Find the next or previous slot when all attendees are available.
|
||||||
|
* @param {number} direction - the search direction (1 or -1)
|
||||||
|
*/
|
||||||
|
Attendees.prototype.findSlot = function(direction) {
|
||||||
|
var _this = this, currentStart;
|
||||||
|
|
||||||
|
this.direction = direction;
|
||||||
|
this.firstStep = true;
|
||||||
|
|
||||||
|
if (direction > 0) {
|
||||||
|
this.findDate = this.forwardFindDate;
|
||||||
|
this.adjustCurrentStart = this.forwardAdjustCurrentStart;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.findDate = this.backwardFindDate;
|
||||||
|
this.adjustCurrentStart = this.backwardAdjustCurrentStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.component.isAllDay) {
|
||||||
|
// Event lasts all day within limits
|
||||||
|
this.start = this.component.start.clone();
|
||||||
|
this.start.setHours(Attendees.dayStartHour);
|
||||||
|
this.start.setMinutes(0);
|
||||||
|
this.start.setSeconds(0);
|
||||||
|
|
||||||
|
this.end = this.component.end.clone();
|
||||||
|
this.end.setHours(Attendees.dayEndHour);
|
||||||
|
this.end.setMinutes(0);
|
||||||
|
this.end.setSeconds(0);
|
||||||
|
|
||||||
|
this.startLimit = Attendees.dayStartHour * 4; // from user's defaults
|
||||||
|
this.endLimit = Attendees.dayEndHour * 4; // from user's defaults
|
||||||
|
|
||||||
|
this.duration = (Attendees.dayEndHour - Attendees.dayStartHour) * 4;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Event can be outside limits
|
||||||
|
this.start = this.component.start;
|
||||||
|
this.end = this.component.end;
|
||||||
|
|
||||||
|
this.startLimit = Attendees.timeToQuarters(this.slotStartTimeLimit); // from time picker
|
||||||
|
this.endLimit = Attendees.timeToQuarters(this.slotEndTimeLimit); // from time picker
|
||||||
|
|
||||||
|
this.duration = Math.ceil((this.end.getTime() - this.start.getTime()) / 900000);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStart = this.component.start.clone();
|
||||||
|
currentStart.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
if (this.workDaysOnly) {
|
||||||
|
this.adjustCurrentStart(currentStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a recursive search
|
||||||
|
return this.step(currentStart).then(function (foundDate) {
|
||||||
|
_this.component.start = new Date(foundDate.getTime());
|
||||||
|
_this.component.end = new Date(_this.component.start.getTime());
|
||||||
|
_this.component.end.addMinutes(_this.component.delta);
|
||||||
|
_this.updateFreeBusyCoverage();
|
||||||
|
return foundDate;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function mergeFreebusy
|
||||||
|
* @memberof Attendees.prototype
|
||||||
|
* @desc Merge freebusy information of organizer and all attendees for a referene date.
|
||||||
|
* @param {date) start - the reference date
|
||||||
|
*/
|
||||||
|
Attendees.prototype.mergeFreebusy = function(start) {
|
||||||
|
var _this = this;
|
||||||
|
var startDay = start.getDayString();
|
||||||
|
|
||||||
|
return this.updateFreeBusy(start).then(function () {
|
||||||
|
var i, j, attendee, attendeeEntries;
|
||||||
|
_this.currentEntries = _.flatMap(_this.component.organizer.freebusy[startDay]);
|
||||||
|
for (i = 0; i < _this.component.attendees.length; i++) {
|
||||||
|
attendee = _this.component.attendees[i];
|
||||||
|
attendeeEntries = _.flatMap(attendee.freebusy[startDay]);
|
||||||
|
for (j = 0; j < _this.currentEntries.length; j++) {
|
||||||
|
_this.currentEntries[j] += attendeeEntries[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function step
|
||||||
|
* @memberof Attendees.prototype
|
||||||
|
* @desc Recursively search for the next available slot, one day a the time.
|
||||||
|
* @param {date) currentStart - the starting day
|
||||||
|
*/
|
||||||
|
Attendees.prototype.step = function(currentStart) {
|
||||||
|
var _this = this;
|
||||||
|
// var currentStartDay = currentStart.getDayString();
|
||||||
|
return this.mergeFreebusy(currentStart).then(function () {
|
||||||
|
var foundDate = _this.findDate(currentStart);
|
||||||
|
if (foundDate) {
|
||||||
|
return foundDate;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
currentStart.addDays(_this.direction > 0 ? 1 : -1);
|
||||||
|
currentStart.setHours(0, 0, 0, 0);
|
||||||
|
if (_this.workDaysOnly) {
|
||||||
|
_this.adjustCurrentStart(currentStart);
|
||||||
|
}
|
||||||
|
return _this.step(currentStart);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
|
@ -31,7 +31,7 @@
|
||||||
* @desc The factory we'll use to register with Angular
|
* @desc The factory we'll use to register with Angular
|
||||||
* @returns the Component constructor
|
* @returns the Component constructor
|
||||||
*/
|
*/
|
||||||
Component.$factory = ['$q', '$timeout', '$log', '$rootScope', 'sgSettings', 'sgComponent_STATUS', 'Preferences', 'User', 'Card', 'Gravatar', 'Resource', function($q, $timeout, $log, $rootScope, Settings, Component_STATUS, Preferences, User, Card, Gravatar, Resource) {
|
Component.$factory = ['$q', '$timeout', '$log', '$rootScope', 'sgSettings', 'sgComponent_STATUS', 'Attendees', 'Preferences', 'User', 'Card', 'Resource', function($q, $timeout, $log, $rootScope, Settings, Component_STATUS, Attendees, Preferences, User, Card, Resource) {
|
||||||
angular.extend(Component, {
|
angular.extend(Component, {
|
||||||
STATUS: Component_STATUS,
|
STATUS: Component_STATUS,
|
||||||
$q: $q,
|
$q: $q,
|
||||||
|
@ -41,8 +41,8 @@
|
||||||
$settings: Settings,
|
$settings: Settings,
|
||||||
$User: User,
|
$User: User,
|
||||||
$Preferences: Preferences,
|
$Preferences: Preferences,
|
||||||
|
$Attendees: Attendees,
|
||||||
$Card: Card,
|
$Card: Card,
|
||||||
$gravatar: Gravatar,
|
|
||||||
$$resource: new Resource(Settings.activeUser('folderURL') + 'Calendar', Settings.activeUser()),
|
$$resource: new Resource(Settings.activeUser('folderURL') + 'Calendar', Settings.activeUser()),
|
||||||
timeFormat: "%H:%M",
|
timeFormat: "%H:%M",
|
||||||
// Filter parameters common to events and tasks
|
// Filter parameters common to events and tasks
|
||||||
|
@ -629,51 +629,20 @@
|
||||||
// this.organizer.$image = Component.$gravatar(this.organizer.email, 32);
|
// this.organizer.$image = Component.$gravatar(this.organizer.email, 32);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
if (this.attendees) {
|
|
||||||
_.forEach(this.attendees, function(attendee) {
|
|
||||||
attendee.image = Component.$gravatar(attendee.email, 32);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load freebusy of attendees
|
|
||||||
this.updateFreeBusy();
|
|
||||||
|
|
||||||
this.selected = false;
|
this.selected = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @function initOrganizer
|
* @function init
|
||||||
* @memberof Component.prototype
|
* @memberof Component.prototype
|
||||||
* @desc Extend instance with organizer including her freebusy information.
|
* @desc Extend instance with required attributes and new data.
|
||||||
* @param {object} calendar - Calendar instance associated to current component
|
* @param {object} data - attributes of component
|
||||||
*/
|
*/
|
||||||
Component.prototype.initOrganizer = function(calendar) {
|
Component.prototype.initAttendees = function() {
|
||||||
var _this = this, promise;
|
this.$attendees = new Component.$Attendees(this);
|
||||||
if (calendar && calendar.isSubscription) {
|
|
||||||
promise = Component.$User.$filter(calendar.owner).then(function(results) {
|
|
||||||
var owner = results[0];
|
|
||||||
_this.organizer = {
|
|
||||||
uid: owner.uid,
|
|
||||||
name: owner.cn,
|
|
||||||
email: owner.c_email
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.organizer = {
|
|
||||||
uid: Component.$settings.activeUser('login'),
|
|
||||||
name: Component.$settings.activeUser('identification'),
|
|
||||||
email: Component.$settings.activeUser('email')
|
|
||||||
};
|
|
||||||
promise = Component.$q.when();
|
|
||||||
}
|
|
||||||
// Fetch organizer's freebusy
|
|
||||||
promise.then(function() {
|
|
||||||
_this.updateFreeBusyAttendee(_this.organizer);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @function hasCustomRepeat
|
* @function hasCustomRepeat
|
||||||
* @memberof Component.prototype
|
* @memberof Component.prototype
|
||||||
|
@ -783,85 +752,6 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @function coversFreeBusy
|
|
||||||
* @memberof Component.prototype
|
|
||||||
* @desc Check if a specific quarter matches the component's period
|
|
||||||
* @returns true if the quarter covers the component's period
|
|
||||||
*/
|
|
||||||
Component.prototype.coversFreeBusy = function(day, hour, quarter) {
|
|
||||||
var b = (angular.isDefined(this.freebusy[day]) &&
|
|
||||||
angular.isDefined(this.freebusy[day][hour]) &&
|
|
||||||
this.freebusy[day][hour][quarter] == 1);
|
|
||||||
return b;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @function updateFreeBusyCoverage
|
|
||||||
* @memberof Component.prototype
|
|
||||||
* @desc Build a 15-minute-based representation of the component's period.
|
|
||||||
* @returns an object literal hashed by days and hours and arrays of four 1's and 0's
|
|
||||||
*/
|
|
||||||
Component.prototype.updateFreeBusyCoverage = function() {
|
|
||||||
var _this = this, freebusy = {};
|
|
||||||
|
|
||||||
if (this.start && this.end) {
|
|
||||||
var roundedStart = new Date(this.start.getTime()),
|
|
||||||
roundedEnd = new Date(this.end.getTime()),
|
|
||||||
startQuarter = parseInt(roundedStart.getMinutes()/15 + 0.5),
|
|
||||||
endQuarter = parseInt(roundedEnd.getMinutes()/15 + 0.5);
|
|
||||||
roundedStart.setMinutes(15*startQuarter);
|
|
||||||
roundedEnd.setMinutes(15*endQuarter);
|
|
||||||
|
|
||||||
_.forEach(roundedStart.daysUpTo(roundedEnd), function(date, index) {
|
|
||||||
var currentDay = date.getDate(),
|
|
||||||
dayKey = date.getDayString(),
|
|
||||||
hourKey;
|
|
||||||
if (dayKey == _this.start.getDayString()) {
|
|
||||||
hourKey = date.getHours().toString();
|
|
||||||
freebusy[dayKey] = {};
|
|
||||||
freebusy[dayKey][hourKey] = [];
|
|
||||||
while (startQuarter > 0) {
|
|
||||||
freebusy[dayKey][hourKey].push(0);
|
|
||||||
startQuarter--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
date = date.beginOfDay();
|
|
||||||
freebusy[dayKey] = {};
|
|
||||||
}
|
|
||||||
while (date.getTime() < _this.end.getTime() &&
|
|
||||||
date.getDate() == currentDay) {
|
|
||||||
hourKey = date.getHours().toString();
|
|
||||||
if (angular.isUndefined(freebusy[dayKey][hourKey]))
|
|
||||||
freebusy[dayKey][hourKey] = [];
|
|
||||||
freebusy[dayKey][hourKey].push(1);
|
|
||||||
date.addMinutes(15);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return freebusy;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @function updateFreeBusy
|
|
||||||
* @memberof Component.prototype
|
|
||||||
* @desc Update the freebusy coverage representation and the attendees freebusy information
|
|
||||||
*/
|
|
||||||
Component.prototype.updateFreeBusy = function() {
|
|
||||||
var _this = this;
|
|
||||||
|
|
||||||
this.freebusy = this.updateFreeBusyCoverage();
|
|
||||||
|
|
||||||
if (this.attendees) {
|
|
||||||
if (this.organizer)
|
|
||||||
this.updateFreeBusyAttendee(this.organizer);
|
|
||||||
_.forEach(this.attendees, function(attendee) {
|
|
||||||
_this.updateFreeBusyAttendee(attendee);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @function setDelta
|
* @function setDelta
|
||||||
* @memberof Component.prototype
|
* @memberof Component.prototype
|
||||||
|
@ -875,70 +765,6 @@
|
||||||
this.end.addMinutes(this.delta);
|
this.end.addMinutes(this.delta);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @function updateFreeBusyAttendee
|
|
||||||
* @memberof Component.prototype
|
|
||||||
* @desc Update the freebusy information for the component's period for a specific attendee.
|
|
||||||
* @param {Object} card - an Card object instance of the attendee
|
|
||||||
*/
|
|
||||||
Component.prototype.updateFreeBusyAttendee = function(attendee) {
|
|
||||||
var resource, uid, params, days;
|
|
||||||
|
|
||||||
if (attendee.uid) {
|
|
||||||
uid = attendee.uid;
|
|
||||||
if (attendee.domain)
|
|
||||||
uid += '@' + attendee.domain;
|
|
||||||
params =
|
|
||||||
{
|
|
||||||
sday: this.start.getDayString(),
|
|
||||||
eday: this.end.getDayString()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (attendee.isMSExchange) {
|
|
||||||
// Attendee is not a local user, but her freebusy data is available from an external MS Exchange server;
|
|
||||||
// we query /SOGo/so/<login_user>/freebusy.ifb/ajaxRead?uid=<uid>
|
|
||||||
resource = Component.$$resource.userResource();
|
|
||||||
params.uid = uid;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Attendee is a user;
|
|
||||||
// web query /SOGo/so/<uid>/freebusy.ifb/ajaxRead
|
|
||||||
resource = Component.$$resource.userResource(uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
days = _.map(this.start.daysUpTo(this.end), function(day) { return day.getDayString(); });
|
|
||||||
|
|
||||||
if (angular.isUndefined(attendee.freebusy))
|
|
||||||
attendee.freebusy = {};
|
|
||||||
|
|
||||||
// Fetch FreeBusy information
|
|
||||||
resource.fetch('freebusy.ifb', 'ajaxRead', params).then(function(data) {
|
|
||||||
_.forEach(days, function(day) {
|
|
||||||
var hour;
|
|
||||||
|
|
||||||
if (angular.isUndefined(attendee.freebusy[day]))
|
|
||||||
attendee.freebusy[day] = {};
|
|
||||||
|
|
||||||
if (angular.isUndefined(data[day]))
|
|
||||||
data[day] = {};
|
|
||||||
|
|
||||||
for (var i = 0; i <= 23; i++) {
|
|
||||||
hour = i.toString();
|
|
||||||
if (data[day][hour])
|
|
||||||
attendee.freebusy[day][hour] = [
|
|
||||||
data[day][hour]["0"],
|
|
||||||
data[day][hour]["15"],
|
|
||||||
data[day][hour]["30"],
|
|
||||||
data[day][hour]["45"]
|
|
||||||
];
|
|
||||||
else
|
|
||||||
attendee.freebusy[day][hour] = [0, 0, 0, 0];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @function getClassName
|
* @function getClassName
|
||||||
* @memberof Component.prototype
|
* @memberof Component.prototype
|
||||||
|
@ -952,101 +778,6 @@
|
||||||
return base + '-folder' + (this.destinationCalendar || this.c_folder || this.pid);
|
return base + '-folder' + (this.destinationCalendar || this.c_folder || this.pid);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @function addAttendee
|
|
||||||
* @memberof Component.prototype
|
|
||||||
* @desc Add an attendee and fetch his freebusy info.
|
|
||||||
* @param {Object} card - an Card object instance to be added to the attendees list
|
|
||||||
*/
|
|
||||||
Component.prototype.addAttendee = function(card, options) {
|
|
||||||
var _this = this, attendee, list, url, params;
|
|
||||||
if (card) {
|
|
||||||
if (!this.attendees || (options && options.organizerCalendar)) {
|
|
||||||
// No attendee yet; initialize the organizer
|
|
||||||
this.initOrganizer(options? options.organizerCalendar : undefined);
|
|
||||||
}
|
|
||||||
if (card.$isList({expandable: true})) {
|
|
||||||
// Decompose list members
|
|
||||||
list = Component.$Card.$find(card.container, card.c_name);
|
|
||||||
list.$id().then(function(listId) {
|
|
||||||
_.forEach(list.refs, function(ref) {
|
|
||||||
attendee = {
|
|
||||||
name: ref.c_cn,
|
|
||||||
email: ref.$preferredEmail(options? options.partial : undefined),
|
|
||||||
role: 'req-participant',
|
|
||||||
partstat: 'needs-action',
|
|
||||||
uid: ref.c_uid,
|
|
||||||
$avatarIcon: 'person',
|
|
||||||
};
|
|
||||||
if (!_.find(_this.attendees, function(o) {
|
|
||||||
return o.email == attendee.email;
|
|
||||||
})) {
|
|
||||||
// Contact is not already an attendee, add it
|
|
||||||
attendee.image = Component.$gravatar(attendee.email, 32);
|
|
||||||
if (_this.attendees)
|
|
||||||
_this.attendees.push(attendee);
|
|
||||||
else
|
|
||||||
_this.attendees = [attendee];
|
|
||||||
_this.updateFreeBusyAttendee(attendee);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Single contact
|
|
||||||
attendee = {
|
|
||||||
uid: card.c_uid,
|
|
||||||
domain: card.c_domain,
|
|
||||||
isMSExchange: card.ismsexchange,
|
|
||||||
name: card.c_cn,
|
|
||||||
email: card.$preferredEmail(),
|
|
||||||
role: 'req-participant',
|
|
||||||
partstat: 'needs-action',
|
|
||||||
$avatarIcon: card.$avatarIcon
|
|
||||||
};
|
|
||||||
if (!_.find(this.attendees, function(o) {
|
|
||||||
return o.email == attendee.email;
|
|
||||||
})) {
|
|
||||||
attendee.image = Component.$gravatar(attendee.email, 32);
|
|
||||||
if (this.attendees)
|
|
||||||
this.attendees.push(attendee);
|
|
||||||
else
|
|
||||||
this.attendees = [attendee];
|
|
||||||
this.updateFreeBusyAttendee(attendee);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @function hasAttendee
|
|
||||||
* @memberof Component.prototype
|
|
||||||
* @desc Verify if one of the email addresses of a Card instance matches an attendee.
|
|
||||||
* @param {Object} card - an Card object instance
|
|
||||||
* @returns true if the Card matches an attendee
|
|
||||||
*/
|
|
||||||
Component.prototype.hasAttendee = function(card) {
|
|
||||||
var attendee = _.find(this.attendees, function(attendee) {
|
|
||||||
return _.find(card.emails, function(email) {
|
|
||||||
return email.value == attendee.email;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return angular.isDefined(attendee);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @function deleteAttendee
|
|
||||||
* @memberof Component.prototype
|
|
||||||
* @desc Remove an attendee from the component
|
|
||||||
* @param {Object} attendee - an object literal defining an attendee
|
|
||||||
*/
|
|
||||||
Component.prototype.deleteAttendee = function(attendee) {
|
|
||||||
var index = _.findIndex(this.attendees, function(currentAttendee) {
|
|
||||||
return currentAttendee.email == attendee.email;
|
|
||||||
});
|
|
||||||
this.attendees.splice(index, 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @function canRemindAttendeesByEmail
|
* @function canRemindAttendeesByEmail
|
||||||
* @memberof Component.prototype
|
* @memberof Component.prototype
|
||||||
|
|
|
@ -205,21 +205,27 @@
|
||||||
/**
|
/**
|
||||||
* @ngInject
|
* @ngInject
|
||||||
*/
|
*/
|
||||||
ComponentEditorController.$inject = ['$rootScope', '$scope', '$log', '$timeout', '$mdDialog', 'sgFocus', 'User', 'CalendarSettings', 'Calendar', 'Component', 'AddressBook', 'Card', 'Alarm', 'stateComponent'];
|
ComponentEditorController.$inject = ['$rootScope', '$scope', '$log', '$timeout', '$element', '$mdDialog', 'sgFocus', 'User', 'CalendarSettings', 'Calendar', 'Component', 'Attendees', 'AddressBook', 'Card', 'Alarm', 'stateComponent'];
|
||||||
function ComponentEditorController($rootScope, $scope, $log, $timeout, $mdDialog, focus, User, CalendarSettings, Calendar, Component, AddressBook, Card, Alarm, stateComponent) {
|
function ComponentEditorController($rootScope, $scope, $log, $timeout, $element, $mdDialog, focus, User, CalendarSettings, Calendar, Component, Attendees, AddressBook, Card, Alarm, stateComponent) {
|
||||||
var vm = this, component, oldStartDate, oldEndDate, oldDueDate;
|
var vm = this, component, oldStartDate, oldEndDate, oldDueDate;
|
||||||
|
|
||||||
this.$onInit = function () {
|
this.$onInit = function () {
|
||||||
|
stateComponent.initAttendees();
|
||||||
this.service = Calendar;
|
this.service = Calendar;
|
||||||
this.component = stateComponent;
|
this.component = stateComponent;
|
||||||
this.categories = {};
|
this.categories = {};
|
||||||
|
this.updateFreeBusyCoverage =
|
||||||
|
angular.bind(this.component.$attendees, this.component.$attendees.updateFreeBusyCoverage);
|
||||||
|
this.coversFreeBusy =
|
||||||
|
angular.bind(this.component.$attendees, this.component.$attendees.coversFreeBusy);
|
||||||
this.showRecurrenceEditor = this.component.$hasCustomRepeat;
|
this.showRecurrenceEditor = this.component.$hasCustomRepeat;
|
||||||
this.showAttendeesEditor = this.component.attendees && this.component.attendees.length;
|
this.showAttendeesEditor = this.component.attendees && this.component.attendees.length;
|
||||||
//this.searchText = null;
|
//this.searchText = null;
|
||||||
this.attendeeConflictError = false;
|
this.attendeeConflictError = false;
|
||||||
this.attendeesEditor = {
|
this.attendeesEditor = {
|
||||||
days: getDays(),
|
days: this.component.$attendees.$days,
|
||||||
hours: getHours()
|
hours: getHours(),
|
||||||
|
containerElement: $element[0].querySelector('#freebusy')
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.component.start)
|
if (this.component.start)
|
||||||
|
@ -297,7 +303,7 @@
|
||||||
card.charCodeAt(i) == 44 || // ,
|
card.charCodeAt(i) == 44 || // ,
|
||||||
card.charCodeAt(i) == 59) && // ;
|
card.charCodeAt(i) == 59) && // ;
|
||||||
emailRE.test(address)) {
|
emailRE.test(address)) {
|
||||||
this.component.addAttendee(createCard(address), options);
|
this.component.$attendees.add(createCard(address), options);
|
||||||
address = '';
|
address = '';
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -305,21 +311,43 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (address)
|
if (address)
|
||||||
this.component.addAttendee(createCard(address), options);
|
this.component.$attendees.add(createCard(address), options);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.component.addAttendee(card, options);
|
this.component.$attendees.add(card, options);
|
||||||
this.showAttendeesEditor |= initOrganizer;
|
this.showAttendeesEditor |= initOrganizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$timeout(scrollToStart);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function scrollToStart() {
|
||||||
|
var dayElement = $element[0].querySelector('#freebusy_day_' + vm.component.start.getDayString());
|
||||||
|
var scrollLeft = dayElement.offsetLeft - vm.attendeesEditor.containerElement.offsetLeft;
|
||||||
|
vm.attendeesEditor.containerElement.scrollLeft = scrollLeft;
|
||||||
|
}
|
||||||
|
|
||||||
this.removeAttendee = function (attendee, form) {
|
this.removeAttendee = function (attendee, form) {
|
||||||
this.component.deleteAttendee(attendee);
|
this.component.$attendees.remove(attendee);
|
||||||
if (this.component.attendees.length === 0)
|
if (this.component.$attendees.getLength() === 0)
|
||||||
this.showAttendeesEditor = false;
|
this.showAttendeesEditor = false;
|
||||||
form.$setDirty();
|
form.$setDirty();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.nextSlot = function () {
|
||||||
|
findSlot(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.previousSlot = function () {
|
||||||
|
findSlot(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
function findSlot(direction) {
|
||||||
|
vm.component.$attendees.findSlot(direction).then(function () {
|
||||||
|
$timeout(scrollToStart);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.priorityLevel = function () {
|
this.priorityLevel = function () {
|
||||||
if (this.component && this.component.priority) {
|
if (this.component && this.component.priority) {
|
||||||
if (this.component.priority > 5)
|
if (this.component.priority > 5)
|
||||||
|
@ -393,18 +421,6 @@
|
||||||
form.$setDirty();
|
form.$setDirty();
|
||||||
};
|
};
|
||||||
|
|
||||||
function getDays() {
|
|
||||||
var days = [];
|
|
||||||
|
|
||||||
if (vm.component.start && vm.component.end)
|
|
||||||
days = vm.component.start.daysUpTo(vm.component.end);
|
|
||||||
|
|
||||||
return _.map(days, function(date) {
|
|
||||||
return { stringWithSeparator: date.stringWithSeparator(),
|
|
||||||
getDayString: date.getDayString() };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHours() {
|
function getHours() {
|
||||||
var hours = [];
|
var hours = [];
|
||||||
for (var i = 0; i <= 23; i++) {
|
for (var i = 0; i <= 23; i++) {
|
||||||
|
@ -486,8 +502,9 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
function updateFreeBusy() {
|
function updateFreeBusy() {
|
||||||
vm.attendeesEditor.days = getDays();
|
vm.component.$attendees.updateFreeBusyCoverage();
|
||||||
vm.component.updateFreeBusy();
|
vm.component.$attendees.updateFreeBusy();
|
||||||
|
scrollToStart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -713,6 +713,7 @@ $quarter_height: 10px;
|
||||||
md-content {
|
md-content {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
display: table-row;
|
display: table-row;
|
||||||
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
md-list {
|
md-list {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
|
|
Loading…
Reference in New Issue