0002150: Freetime-search function
authorPaul Mehrer <p.mehrer@metaways.de>
Tue, 27 Jun 2017 08:46:43 +0000 (10:46 +0200)
committersstamer <s.stamer@metaways.de>
Fri, 30 Jun 2017 11:51:58 +0000 (13:51 +0200)
Change-Id: I055e5a9226db35062d523dc1eb0ec2b2d57ec84a
Reviewed-on: http://gerrit.tine20.com/customers/4947
Tested-by: Jenkins CI (http://ci.tine20.com/)
Reviewed-by: sstamer <s.stamer@metaways.de>
Tested-by: sstamer <s.stamer@metaways.de>
18 files changed:
tests/tine20/Calendar/JsonTests.php
tine20/Calendar/Controller/Event.php
tine20/Calendar/Frontend/Json.php
tine20/Calendar/css/Calendar.css
tine20/Calendar/js/AbstractView.js
tine20/Calendar/js/AttendeeGridPanel.js
tine20/Calendar/js/Calendar.js
tine20/Calendar/js/ColorManager.js
tine20/Calendar/js/DaysView.js
tine20/Calendar/js/DaysViewEventUI.js
tine20/Calendar/js/EventEditDialog.js
tine20/Calendar/js/EventFinderOptionsDialog.js
tine20/Calendar/js/EventUI.js
tine20/Calendar/js/FreeTimeSearchDialog.js [new file with mode: 0644]
tine20/Calendar/js/Model.js
tine20/Tinebase/Server/WebDAV.php
tine20/Tinebase/css/Tinebase.css
tine20/Tinebase/css/ux/display/DisplayPanel.css

index 354bee0..466435b 100644 (file)
@@ -2051,6 +2051,7 @@ class Calendar_JsonTests extends Calendar_TestCase
         $event->originator_tz = $event->dtstart->getTimezone()->getName();
 
         $options = array(
+            'from'        => $event->dtstart->getClone()->addDay(2)->setHour(12),
             'constraints' => array(array(
                 'dtstart'   => $event->dtstart->getClone()->setHour(10),
                 'dtend'     => $event->dtstart->getClone()->setHour(22),
@@ -2058,11 +2059,78 @@ class Calendar_JsonTests extends Calendar_TestCase
             )),
         );
 
-        $expectedDtStart = new Tinebase_DateTime('2009-03-27 10:00:00', $event->originator_tz);
+        $expectedDtStart = new Tinebase_DateTime('2009-03-27 12:00:00', $event->originator_tz);
         $expectedDtStart->setTimezone(Tinebase_Core::getUserTimezone());
 
         $result = $this->_uit->searchFreeTime($event->toArray(), $options);
-        static::assertTrue(is_array($result) && count($result) === 3 && count($result['results']) === 1);
+        static::assertTrue(is_array($result) && count($result) === 4 && count($result['results']) === 1);
+        static::assertEquals($expectedDtStart->toString(), $result['results'][0]['dtstart']);
+    }
+
+    public function testSearchFreeTime1()
+    {
+        // 2009-03-25 => Mittwoch
+        $event = $this->_getEvent();
+        $event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender', array(
+            array('user_id' => $this->_getPersonasContacts('sclever')->getId(), 'user_type' => Calendar_Model_Attender::USERTYPE_USER),
+            array('user_id' => $this->_getPersonasContacts('pwulf')->getId(), 'user_type' => Calendar_Model_Attender::USERTYPE_USER)
+        ));
+        $event->originator_tz = $event->dtstart->getTimezone()->getName();
+
+        $options = array(
+            'constraints' => array(array(
+                'dtstart'   => $event->dtstart->getClone()->subDay(1)->setHour(10),
+                'dtend'     => $event->dtstart->getClone()->subDay(1)->setHour(22),
+                'rrule'     => 'FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU'
+            ), array(
+                'dtstart'   => $event->dtstart->getClone()->subDay(1)->setHour(13),
+                'dtend'     => $event->dtstart->getClone()->subDay(1)->setHour(16),
+                'rrule'     => 'FREQ=WEEKLY;INTERVAL=1;BYDAY=TH,FR'
+            )),
+        );
+
+        $expectedDtStart = new Tinebase_DateTime('2009-03-26 13:00:00', $event->originator_tz);
+        $expectedDtStart->setTimezone(Tinebase_Core::getUserTimezone());
+
+        $result = $this->_uit->searchFreeTime($event->toArray(), $options);
+        static::assertTrue(is_array($result) && count($result) === 4 && count($result['results']) === 1);
+        static::assertEquals($expectedDtStart->toString(), $result['results'][0]['dtstart']);
+    }
+
+    public function testSearchFreeTime2()
+    {
+        // 2009-03-25 => Mittwoch
+        $event = $this->_getEvent();
+        $event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender', array(
+            array('user_id' => $this->_getPersonasContacts('sclever')->getId(), 'user_type' => Calendar_Model_Attender::USERTYPE_USER),
+            array('user_id' => $this->_getPersonasContacts('pwulf')->getId(), 'user_type' => Calendar_Model_Attender::USERTYPE_USER)
+        ));
+        $event->originator_tz = $event->dtstart->getTimezone()->getName();
+
+        $options = array(
+            'constraints' => array(array(
+                'dtstart'   => $event->dtstart->getClone()->subDay(1)->setHour(10),
+                'dtend'     => $event->dtstart->getClone()->subDay(1)->setHour(22),
+                'rrule'     => 'FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU'
+            ), array(
+                'dtstart'   => $event->dtstart->getClone()->subDay(1)->setHour(13),
+                'dtend'     => $event->dtstart->getClone()->subDay(1)->setHour(16),
+                'rrule'     => 'FREQ=WEEKLY;INTERVAL=1;BYDAY=TH,FR'
+            )),
+        );
+
+        $expectedDtStart = new Tinebase_DateTime('2009-03-27 13:00:00', $event->originator_tz);
+        $expectedDtStart->setTimezone(Tinebase_Core::getUserTimezone());
+
+        $event->dtstart->addDay(1)->setTime(16, 0 ,0);
+        $event->dtend->addDay(1)->setTime(17, 0 ,0);
+
+        $createEvent = new Calendar_Model_Event(array(), true);
+        $createEvent->setFromJsonInUsersTimezone($event->toArray());
+        Calendar_Controller_Event::getInstance()->create($createEvent);
+
+        $result = $this->_uit->searchFreeTime($event->toArray(), $options);
+        static::assertTrue(is_array($result) && count($result) === 4 && count($result['results']) === 1);
         static::assertEquals($expectedDtStart->toString(), $result['results'][0]['dtstart']);
     }
 }
index 6543a81..10e1cb8 100644 (file)
@@ -360,6 +360,40 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
     }
 
     /**
+     * @param Tinebase_Record_RecordSet $_records
+     * @return array
+     */
+    public function mergeFreeBusyInfo(Tinebase_Record_RecordSet $_records)
+    {
+        $_records->sort('dtstart');
+        $result = array();
+        /** @var Calendar_Model_Event $event */
+        foreach($_records as $event) {
+            foreach($result as $key => &$period) {
+                if ($event->dtstart->isEarlierOrEquals($period['dtend'])) {
+                    if ($event->dtend->isLaterOrEquals($period['dtstart'])) {
+                        if ($event->dtstart->isEarlier($period['dtstart'])) {
+                            $period['dtstart'] = clone $event->dtstart;
+                        }
+                        if ($event->dtend->isLater($period['dtend'])) {
+                            $period['dtend'] = clone $event->dtend;
+                        }
+                        continue 2;
+                    } else {
+                        throw new Tinebase_Exception_UnexpectedValue('record set sort by dtstart did not work!');
+                    }
+                }
+            }
+            $result[] = array(
+                'dtstart' => $event->dtstart,
+                'dtend' => $event->dtend
+            );
+        }
+
+        return $result;
+    }
+
+    /**
      * returns freebusy information for given period and given attendee
      * 
      * @todo merge overlapping events to one freebusy entry
@@ -609,6 +643,8 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
      */
     public function searchFreeTime($_event, $_options)
     {
+        $functionTime = time();
+
         // validate $_event, originator_tz will be validated by setTimezone() call
         if (!isset($_event->dtstart) || !$_event->dtstart instanceof Tinebase_DateTime) {
             throw new Tinebase_Exception_UnexpectedValue('dtstart needs to be set');
@@ -630,7 +666,7 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
         $until = isset($_options['until']) ? ($_options['until'] instanceof Tinebase_DateTime ? $_options['until'] :
             new Tinebase_DateTime($_options['until'])) : $_event->dtend->getClone()->addYear(2);
 
-        $currentFrom = $from->getClone()->setTime(0, 0, 0);
+        $currentFrom = $from->getClone();
         $currentUntil = $from->getClone()->addDay(6)->setTime(23, 59, 59);
         if ($currentUntil->isLater($until)) {
             $currentUntil = clone $until;
@@ -667,6 +703,12 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
         }
 
         do {
+            if (time() - $functionTime > 23) {
+                $exception = new Calendar_Exception_AttendeeBusy();
+                $exception->setEvent(new Calendar_Model_Event(array('dtend' => $currentFrom), true));
+                throw $exception;
+            }
+
             $currentConstraints = clone $constraints;
             Calendar_Model_Rrule::mergeRecurrenceSet($currentConstraints, $currentFrom, $currentUntil);
             $currentConstraints->sort('dtstart');
@@ -679,7 +721,11 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
                     $recurEvent = clone $_event;
                     $recurEvent->uid = Tinebase_Record_Abstract::generateUID();
                     $recurEvent->dtstart = $event->dtstart->getClone()->subDay(1);
-                    $recurEvent->dtend = $recurEvent->dtstart->getClone()->addSecond($durationSec);
+                    if ($_event->is_all_day_event) {
+                        $recurEvent->dtend = $event->dtend->getClone()->subDay(1);
+                    } else {
+                        $recurEvent->dtend = $recurEvent->dtstart->getClone()->addSecond($durationSec);
+                    }
                     if (null === ($recurEvent = Calendar_Model_Rrule::computeNextOccurrence($recurEvent, $exceptions, $event->dtstart))
                             || $recurEvent->dtstart->isLater($event->dtend)) {
                         $remove[] = $event;
@@ -705,51 +751,75 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
                 }
 
                 $busySlots = $this->getFreeBusyInfo(new Calendar_Model_EventFilter($periods, Tinebase_Model_Filter_FilterGroup::CONDITION_OR), $_event->attendee);
-                $busySlots->sort('dtstart');
+                $busySlots = $this->mergeFreeBusyInfo($busySlots);
 
                 /** @var Calendar_Model_Event $event */
                 foreach ($currentConstraints as $event) {
+                    if ($event->dtend->isEarlierOrEquals($currentFrom)) {
+                        continue;
+                    }
+
+                    if ($_event->is_all_day_event) {
+                        $durationSec = (int)$event->dtend->getTimestamp() - (int)$event->dtstart->getTimestamp();
+                    }
+
                     $constraintStart = (int)$event->dtstart->getTimestamp();
+                    if ($constraintStart < (int)$currentFrom->getTimestamp()) {
+                        $constraintStart = (int)$currentFrom->getTimestamp();
+                    }
                     $constraintEnd = (int)$event->dtend->getTimestamp();
+                    if ($constraintEnd > (int)$currentUntil->getTimestamp()) {
+                        $constraintEnd = (int)$currentUntil->getTimestamp();
+                    }
                     $lastBusyEnd = $constraintStart;
                     $remove = array();
                     /** @var Calendar_Model_FreeBusy $busy */
-                    foreach ($busySlots as $busy) {
-                        $busyStart = (int)$busy->dtstart->getTimestamp();
-                        $busyEnd = (int)$busy->dtend->getTimestamp();
+                    foreach ($busySlots as $key => $busy) {
+                        $busyStart = (int)$busy['dtstart']->getTimestamp();
+                        $busyEnd = (int)$busy['dtend']->getTimestamp();
 
                         if ($busyEnd < $constraintStart) {
-                            $remove[] = $busy;
+                            $remove[] = $key;
                             continue;
                         }
 
-                        if ($lastBusyEnd + $durationSec <= $busyStart) {
+                        if ($busyStart > ($constraintEnd - $durationSec)) {
+                            $lastBusyEnd = $busyEnd;
+                            break;
+                        }
+
+                        if (($lastBusyEnd + $durationSec) <= $busyStart) {
                             // check between $lastBusyEnd and $busyStart
                             $result = $this->_tryForFreeSlot($_event, $lastBusyEnd, $busyStart, $durationSec, $until);
                             if ($result->count() > 0) {
+                                if ($_event->is_all_day_event) {
+                                    $result->getFirstRecord()->dtstart = $_event->dtstart;
+                                    $result->getFirstRecord()->dtend = $_event->dtend;
+                                }
                                 return $result;
                             }
                         }
                         $lastBusyEnd = $busyEnd;
-                        if ($busyStart > $constraintEnd - $durationSec) {
-                            break;
-                        }
                     }
-                    foreach ($remove as $record) {
-                        $busySlots->removeRecord($record);
+                    foreach ($remove as $key) {
+                        unset($busySlots[$key]);
                     }
 
-                    if ($lastBusyEnd + $durationSec <= $constraintEnd) {
+                    if (($lastBusyEnd + $durationSec) <= $constraintEnd) {
                         // check between $lastBusyEnd and $constraintEnd
                         $result = $this->_tryForFreeSlot($_event, $lastBusyEnd, $constraintEnd, $durationSec, $until);
                         if ($result->count() > 0) {
+                            if ($_event->is_all_day_event) {
+                                $result->getFirstRecord()->dtstart = $_event->dtstart;
+                                $result->getFirstRecord()->dtend = $_event->dtend;
+                            }
                             return $result;
                         }
                     }
                 }
             }
 
-            $currentFrom->addDay(7);
+            $currentFrom->addDay(7)->setTime(0, 0, 0);
             $currentUntil->addDay(7);
             if ($currentUntil->isLater($until)) {
                 $currentUntil = clone $until;
index 626bd35..71c696f 100644 (file)
@@ -291,15 +291,41 @@ class Calendar_Frontend_Json extends Tinebase_Frontend_Json_Abstract
         $eventRecord = new Calendar_Model_Event(array(), TRUE);
         $eventRecord->setFromJsonInUsersTimezone($_event);
 
-        $records = Calendar_Controller_Event::getInstance()->searchFreeTime($eventRecord, $_options);
+        if (isset($_options['from']) || isset($_options['until'])) {
+            $tmpData = array();
+            if (isset($_options['from'])) {
+                $tmpData['dtstart'] = $_options['from'];
+            }
+            if (isset($_options['until'])) {
+                $tmpData['dtend'] = $_options['until'];
+            }
+            $tmpEvent = new Calendar_Model_Event(array(), TRUE);
+            $tmpEvent->setFromJsonInUsersTimezone($tmpData);
+            if (isset($_options['from'])) {
+                $_options['from'] = $tmpEvent->dtstart;
+            }
+            if (isset($_options['until'])) {
+                $_options['until'] = $tmpEvent->dtend;
+            }
+        }
+
+        $timeSearchStopped = null;
+        try {
+            $records = Calendar_Controller_Event::getInstance()->searchFreeTime($eventRecord, $_options);
+        } catch (Calendar_Exception_AttendeeBusy $ceab) {
+            $event = $this->_recordToJson($ceab->getEvent());
+            $timeSearchStopped = $event['dtend'];
+            $records = new Tinebase_Record_RecordSet('Calendar_Model_Event', array());
+        }
 
         $records->attendee = array();
         $result = $this->_multipleRecordsToJson($records, null, null);
 
         return array(
-            'results'       => $result,
-            'totalcount'    => count($result),
-            'filter'        => array(),
+            'results'           => $result,
+            'totalcount'        => count($result),
+            'filter'            => array(),
+            'timeSearchStopped' => $timeSearchStopped,
         );
     }
     
index 959999c..57d456e 100644 (file)
     background-image:url(../../images/oxygen/32x32/actions/view-calendar-workweek.png) !important;
 }
 
+.action_fretimesearch {
+    background-image:url(../../images/oxygen/16x16/actions/system-search.png) !important;
+}
+.x-btn-medium .action_fretimesearch {
+    background-image:url(../../images/oxygen/22x22/actions/system-search.png) !important;
+}
+.x-btn-large .action_fretimesearch {
+    background-image:url(../../images/oxygen/32x32/actions/system-search.png) !important;
+}
+
 .cal-attendee-type-resource {
     background-image:url(../../images/oxygen/16x16/devices/video-projector.png) !important;
     background-position:2px 2px;
index e062c0c..0070245 100644 (file)
@@ -181,6 +181,7 @@ Tine.Calendar.AbstractView = Ext.extend(Ext.Container, {
             return;
         }
 
+        this.removeEvent(event);
         var registry = this.getParallelEventRegistry(event);
         var parallelEvents = registry.getEvents(event.get('dtstart'), event.get('dtend'));
         for (var j=0; j<parallelEvents.length; j++) {
index 4626da9..27119d1 100644 (file)
@@ -278,8 +278,8 @@ Tine.Calendar.AttendeeGridPanel = Ext.extend(Ext.grid.EditorGridPanel, {
                     height: 170,
                     scope: this,
                     options: [
-                        {text: 'Group', name: 'sel_group'},
-                        {text: 'Member of Group', name: 'sel_memberOf'}
+                        {text: this.app.i18n._('Group'), name: 'sel_group'},
+                        {text: this.app.i18n._('Member of Group'), name: 'sel_memberOf'}
                     ],
 
                     handler: function(option) {
index 771a659..c25b95e 100644 (file)
@@ -102,8 +102,6 @@ Tine.Calendar.MainScreen = function(config) {
     var prefs = this.app.getRegistry().get('preferences');
     Ext.DatePicker.prototype.startDay = parseInt((prefs ? prefs.get('firstdayofweek') : 1), 10);
 
-    Tine.Calendar.colorMgr = new Tine.Calendar.ColorManager({});
-    
     Tine.Calendar.MainScreen.superclass.constructor.apply(this, arguments);
 };
 
index 5f1d445..95de42f 100644 (file)
@@ -85,7 +85,9 @@ Ext.apply(Tine.Calendar.ColorManager.prototype, {
     },
     
     getStrategy: function() {
-        var name = Tine.Calendar.ColorManager.colorStrategyBtn.colorStrategy;
+        var p = Tine.Calendar.ColorManager.colorStrategyBtn.prototype,
+            name = Ext.state.Manager.get(p.stateId, p).colorStrategy;
+
         return Tine.Calendar.colorStrategies[name];
     },
     
@@ -125,6 +127,8 @@ Ext.apply(Tine.Calendar.ColorManager.prototype, {
     }
 });
 
+Tine.Calendar.colorMgr = new Tine.Calendar.ColorManager({});
+
 Tine.Calendar.ColorManager.str2dec = function(string) {
     var s = String(string).replace('#', ''),
         parts = s.match(/([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})/);
@@ -198,7 +202,7 @@ Tine.Calendar.ColorManager.colorStrategyBtn = Ext.extend(Ext.Button, {
         };
 
         Tine.Calendar.ColorManager.colorStrategyBtn.superclass.initComponent.apply(this, arguments);
-        Tine.Calendar.ColorManager.colorStrategyBtn = this;
+        // Tine.Calendar.ColorManager.colorStrategyBtn = this;
     },
 
     changeColorStrategy: function(strategy) {
index 6f94813..ed9224d 100644 (file)
@@ -148,6 +148,11 @@ Ext.extend(Tine.Calendar.DaysView, Tine.Calendar.AbstractView, {
      * deny drag action if edit grant for event is missing
      */
     denyDragOnMissingEditGrant: true,
+    /**
+     * @cfg {Boolean} readOnly
+     * no dd acionts if read only
+     */
+    readOnly: false,
 
 
     /**
@@ -196,7 +201,7 @@ Ext.extend(Tine.Calendar.DaysView, Tine.Calendar.AbstractView, {
         this.startDate = period.from;
         
         var tbar = this.findParentBy(function(c) {return c.getTopToolbar()}).getTopToolbar();
-        if (tbar) {
+        if (tbar && tbar.periodPicker) {
             tbar.periodPicker.update(this.startDate);
             this.startDate = tbar.periodPicker.getPeriod().from;
         }
@@ -237,7 +242,9 @@ Ext.extend(Tine.Calendar.DaysView, Tine.Calendar.AbstractView, {
 
         this.initTimeScale();
 
-        this.mon(Tine.Tinebase.MainScreen, 'appactivate', this.onAppActivate, this);
+        if (Tine.Tinebase.MainScreen) {
+            this.mon(Tine.Tinebase.MainScreen, 'appactivate', this.onAppActivate, this);
+        }
 
         // apply preferences
         var prefs = this.app.getRegistry().get('preferences'),
@@ -420,7 +427,7 @@ Ext.extend(Tine.Calendar.DaysView, Tine.Calendar.AbstractView, {
                     
                     // don't allow dragging of dirty events
                     // don't allow dragging with missing edit grant
-                    if (! event || event.dirty || (this.view.denyDragOnMissingEditGrant && ! event.get('editGrant'))) {
+                    if (! event || event.dirty || this.readOnly || (this.view.denyDragOnMissingEditGrant && ! event.get('editGrant'))) {
                         return;
                     }
                     
@@ -894,7 +901,7 @@ Ext.extend(Tine.Calendar.DaysView, Tine.Calendar.AbstractView, {
         var dtStart = this.getTargetDateTime(e),
             dtEnd = this.getTargetDateTime(e, this.timeIncrement, 's');
 
-        if (dtStart) {
+        if (dtStart && !this.readOnly) {
             var newId = 'cal-daysviewpanel-new-' + Ext.id();
             var event = new Tine.Calendar.Model.Event(Ext.apply(Tine.Calendar.Model.Event.getDefaultData(), {
                 id: newId,
index c4b0422..c313770 100644 (file)
@@ -150,7 +150,7 @@ Tine.Calendar.DaysViewEventUI = Ext.extend(Tine.Calendar.EventUI, {
             eventEl.setOpacity(0.5);
         }
 
-        if (! (this.endColNum > view.numOfDays) && this.event.get('editGrant')) {
+        if (! (this.endColNum > view.numOfDays) && this.event.get('editGrant') && !view.readOnly) {
             this.resizeable = new Ext.Resizable(eventEl, {
                 handles: 'e',
                 disableTrackOver: true,
@@ -219,7 +219,7 @@ Tine.Calendar.DaysViewEventUI = Ext.extend(Tine.Calendar.EventUI, {
                 eventEl.setOpacity(0.5);
             }
 
-            if (currColNum == this.endColNum && this.event.get('editGrant')) {
+            if (currColNum == this.endColNum && this.event.get('editGrant') && !view.readOnly) {
                 this.resizeable = new Ext.Resizable(eventEl, {
                     handles: 's',
                     disableTrackOver: true,
index 05c1915..8f0833e 100644 (file)
@@ -10,6 +10,8 @@
  
 Ext.ns('Tine.Calendar');
 
+require('./FreeTimeSearchDialog');
+
 /**
  * @namespace Tine.Calendar
  * @class Tine.Calendar.EventEditDialog
@@ -291,6 +293,22 @@ Tine.Calendar.EventEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
         };
     },
 
+    onFreeTimeSearch: function() {
+        this.onRecordUpdate();
+        Tine.Calendar.FreeTimeSearchDialog.openWindow({
+            record: this.record,
+            listeners: {
+                scope: this,
+                apply: this.onFreeTimeSearchApply
+            }
+        });
+    },
+
+    onFreeTimeSearchApply: function(dialog, recordData) {
+        this.record = this.recordProxy.recordReader({responseText: recordData});
+        this.onRecordLoad();
+    },
+
     /**
      * mute first alert
      * 
@@ -318,6 +336,12 @@ Tine.Calendar.EventEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
         );
 
         this.tbarItems = [new Ext.Button(new Ext.Action({
+            text: Tine.Tinebase.appMgr.get('Calendar').i18n._('Free Time Search'),
+            handler: this.onFreeTimeSearch,
+            iconCls: 'action_fretimesearch',
+            disabled: false,
+            scope: this
+        })), new Ext.Button(new Ext.Action({
             text: Tine.Tinebase.appMgr.get('Calendar').i18n._('Mute Notification'),
             handler: this.onMuteNotificationOnce,
             iconCls: 'notes_noteIcon',
index f9af0fa..2a0e96d 100644 (file)
@@ -18,13 +18,13 @@ Ext.ns('Tine.Calendar');
  */
 Tine.Calendar.EventFinderOptionsDialog = Ext.extend(Ext.Panel, {
     defaultOptions: [
-        {id: 'monday', active: true, config: [8, 18.00]},
-        {id: 'tuesday', active: true, config: [8, 18.00]},
-        {id: 'wednesday', active: true, config: [8, 18.00]},
-        {id: 'thursday', active: true, config: [8, 18.00]},
-        {id: 'friday', active: true, config: [8, 18.00]},
-        {id: 'monday', active: false, config: [8, 18.00]},
-        {id: 'monday', active: false, config: [8, 18.00]}
+        {id: 'MO', active: true, config: [8, 18.00], period: {from: '08:00:00', until: '18:00:00' }},
+        {id: 'TU', active: true, config: [8, 18.00], period: {from: '08:00:00', until: '18:00:00' }},
+        {id: 'WE', active: true, config: [8, 18.00], period: {from: '08:00:00', until: '18:00:00' }},
+        {id: 'TH', active: true, config: [8, 18.00], period: {from: '08:00:00', until: '18:00:00' }},
+        {id: 'FR', active: true, config: [8, 18.00], period: {from: '08:00:00', until: '18:00:00' }},
+        {id: 'SA', active: false, config: [8, 18.00], period: {from: '08:00:00', until: '18:00:00' }},
+        {id: 'SU', active: false, config: [8, 18.00], period: {from: '08:00:00', until: '18:00:00' }}
     ],
 
     windowNamePrefix: 'eventfinderoptionsdialog_',
@@ -36,23 +36,23 @@ Tine.Calendar.EventFinderOptionsDialog = Ext.extend(Ext.Panel, {
 
     layout: 'fit',
 
-    wkdays: ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
+    wkdays: ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'],
 
-    mondaySlider: null,
-    tuesdaySlider: null,
-    wednesdaySlider: null,
-    thursdaySlider: null,
-    fridaySlider: null,
-    saturdaySlider: null,
-    sundaySlider: null,
+    MOSlider: null,
+    TUSlider: null,
+    WESlider: null,
+    THSlider: null,
+    FRSlider: null,
+    SASlider: null,
+    SUSlider: null,
 
-    mondayCheckbox: null,
-    tuesdayCheckbox: null,
-    wednesdayCheckbox: null,
-    thursdayCheckbox: null,
-    fridayCheckbox: null,
-    saturdayCheckbox: null,
-    sundayCheckbox: null,
+    MOCheckbox: null,
+    TUCheckbox: null,
+    WECheckbox: null,
+    THCheckbox: null,
+    FRCheckbox: null,
+    SACheckbox: null,
+    SUCheckbox: null,
 
     stateId: 'eventFinderOptions',
     stateConfig: [],
@@ -153,44 +153,44 @@ Tine.Calendar.EventFinderOptionsDialog = Ext.extend(Ext.Panel, {
 
     onSaveAndClose: function () {
         var data = [{
-            id: 'monday',
-            active: this.mondayCheckbox.getValue(),
-            config: this.mondaySlider.getRange(),
-            period: this.getPeriodFromSliderRange(this.mondaySlider.getRange())
+            id: 'MO',
+            active: this.MOCheckbox.getValue(),
+            config: this.MOSlider.getRange(),
+            period: this.getPeriodFromSliderRange(this.MOSlider.getRange())
         }, {
-            id: 'tuesday',
-            active: this.tuesdayCheckbox.getValue(),
-            config: this.tuesdaySlider.getRange(),
-            period: this.getPeriodFromSliderRange(this.tuesdaySlider.getRange())
+            id: 'TU',
+            active: this.TUCheckbox.getValue(),
+            config: this.TUSlider.getRange(),
+            period: this.getPeriodFromSliderRange(this.TUSlider.getRange())
         }, {
-            id: 'wednesday',
-            active: this.wednesdayCheckbox.getValue(),
-            config: this.wednesdaySlider.getRange(),
-            period: this.getPeriodFromSliderRange(this.wednesdaySlider.getRange())
+            id: 'WE',
+            active: this.WECheckbox.getValue(),
+            config: this.WESlider.getRange(),
+            period: this.getPeriodFromSliderRange(this.WESlider.getRange())
         }, {
-            id: 'thursday',
-            active: this.thursdayCheckbox.getValue(),
-            config: this.thursdaySlider.getRange(),
-            period: this.getPeriodFromSliderRange(this.thursdaySlider.getRange())
+            id: 'TH',
+            active: this.THCheckbox.getValue(),
+            config: this.THSlider.getRange(),
+            period: this.getPeriodFromSliderRange(this.THSlider.getRange())
         }, {
-            id: 'friday',
-            active: this.fridayCheckbox.getValue(),
-            config: this.fridaySlider.getRange(),
-            period: this.getPeriodFromSliderRange(this.fridaySlider.getRange())
+            id: 'FR',
+            active: this.FRCheckbox.getValue(),
+            config: this.FRSlider.getRange(),
+            period: this.getPeriodFromSliderRange(this.FRSlider.getRange())
         }, {
-            id: 'saturday',
-            active: this.saturdayCheckbox.getValue(),
-            config: this.saturdaySlider.getRange(),
-            period: this.getPeriodFromSliderRange(this.saturdaySlider.getRange())
+            id: 'SA',
+            active: this.SACheckbox.getValue(),
+            config: this.SASlider.getRange(),
+            period: this.getPeriodFromSliderRange(this.SASlider.getRange())
         }, {
-            id: 'sunday',
-            active: this.sundayCheckbox.getValue(),
-            config: this.sundaySlider.getRange(),
-            period: this.getPeriodFromSliderRange(this.sundaySlider.getRange())
+            id: 'SU',
+            active: this.SUCheckbox.getValue(),
+            config: this.SUSlider.getRange(),
+            period: this.getPeriodFromSliderRange(this.SUSlider.getRange())
         }];
 
         Ext.state.Manager.set(this.stateId, data);
-        this.fireEvent('apply', this, data);
+        this.fireEvent('apply', this, Ext.encode(data));
         this.window.close();
     },
 
@@ -216,7 +216,7 @@ Tine.Calendar.EventFinderOptionsDialog = Ext.extend(Ext.Panel, {
                 {
                     xtype: 'checkbox',
                     checked: !!config && config.active,
-                    boxLabel: _.capitalize(id),
+                    boxLabel: Date.dayNames[this.wkdays.indexOf(id)],
                     name: id,
                     anchor: '95%',
                     flex: 1,
index cd63e48..c1d63af 100644 (file)
@@ -24,7 +24,19 @@ Tine.Calendar.EventUI.prototype = {
             el.addClass(cls);
         });
     },
-    
+
+    removeClass: function(cls) {
+        Ext.each(this.getEls(), function(el){
+            el.removeClass(cls);
+        });
+    },
+
+    focus: function() {
+        Ext.each(this.getEls(), function(el){
+            el.focus();
+        });
+    },
+
     blur: function() {
         Ext.each(this.getEls(), function(el){
             el.blur();
@@ -32,17 +44,31 @@ Tine.Calendar.EventUI.prototype = {
     },
     
     clearDirty: function() {
-        Ext.each(this.getEls(), function(el) {
-            el.setOpacity(1, 1);
+        this.setOpacity(1, 1);
+    },
+
+    setStyle: function(style) {
+        Ext.each(this.getEls(), function(el){
+            el.setStyle(style);
         });
     },
-    
-    focus: function() {
+
+    getStyle: function(property) {
+        var value;
         Ext.each(this.getEls(), function(el){
-            el.focus();
+            value = el.getStyle(property);
+            return false;
         });
+
+        return value;
     },
-    
+
+    setOpacity: function(v, a) {
+        Ext.each(this.getEls(), function(el){
+            el.setOpacity(v, a);
+        });
+    },
+
     /**
      * returns events dom
      * @return {Array} of Ext.Element
@@ -64,11 +90,9 @@ Tine.Calendar.EventUI.prototype = {
     },
     
     markDirty: function() {
-        Ext.each(this.getEls(), function(el) {
-            el.setOpacity(0.5, 1);
-        });
+        this.setOpacity(0.5, 1);
     },
-    
+
     markOutOfFilter: function() {
         Ext.each(this.getEls(), function(el) {
             el.setOpacity(0.5, 0);
@@ -109,13 +133,7 @@ Tine.Calendar.EventUI.prototype = {
         }
         this.domIds = [];
     },
-    
-    removeClass: function(cls) {
-        Ext.each(this.getEls(), function(el){
-            el.removeClass(cls);
-        });
-    },
-    
+
     render: function(view) {
         this.event.view = view;
 
@@ -142,30 +160,7 @@ Tine.Calendar.EventUI.prototype = {
 
         // compute status icons
         this.statusIcons = Tine.Calendar.EventUI.getStatusInfo(this.event, this.attendeeRecord);
-    },
-    
-    setOpacity: function(v) {
-        Ext.each(this.getEls(), function(el){
-            el.setStyle(v);
-        });
-    },
-    
-    setStyle: function(style) {
-        Ext.each(this.getEls(), function(el){
-            el.setStyle(style);
-        });
-    },
-
-    getStyle: function(property) {
-        var value;
-        Ext.each(this.getEls(), function(el){
-            value = el.getStyle(property);
-            return false;
-        });
-
-        return value;
     }
-    
 };
 
 Tine.Calendar.EventUI.getStatusInfo = function(event, attendeeRecord) {
diff --git a/tine20/Calendar/js/FreeTimeSearchDialog.js b/tine20/Calendar/js/FreeTimeSearchDialog.js
new file mode 100644 (file)
index 0000000..7e07430
--- /dev/null
@@ -0,0 +1,326 @@
+/*
+ * Tine 2.0
+ *
+ * @package     Calendar
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Cornelius Weiss <c.weiss@metaways.de>
+ * @copyright   Copyright (c) 2017 Metaways Infosystems GmbH (http://www.metaways.de)
+ */
+
+Ext.ns('Tine.Calendar');
+
+/**
+ * @namespace Tine.Calendar
+ * @class Tine.Calendar.FreeTimeSearchDialog
+ * @extends Ext.Panel
+ */
+Tine.Calendar.FreeTimeSearchDialog = Ext.extend(Ext.Panel, {
+
+    cls: 'tw-editdialog',
+    layout: 'fit',
+    windowNamePrefix: 'CalFreeTimeSearchWindow_',
+    optionsStateId: 'FreeTimeSearchOptions',
+
+    initComponent: function() {
+        var _ = window.lodash;
+        this.app = Tine.Tinebase.appMgr.get('Calendar');
+        this.window.setTitle(this.app.i18n._('Free Time Search'));
+
+        this.recordClass = Tine.Calendar.Model.Event;
+        this.recordProxy = Tine.Calendar.backend;
+
+        if (Ext.isString(this.record)) {
+            this.record = this.recordProxy.recordReader({responseText: this.record});
+        }
+
+        var prefs = this.app.getRegistry().get('preferences');
+        Ext.DatePicker.prototype.startDay = parseInt((prefs ? prefs.get('firstdayofweek') : 1), 10);
+
+        this.store = new Ext.data.JsonStore({
+            fields: Tine.Calendar.Model.Event,
+            proxy: Tine.Calendar.backend,
+            reader: new Ext.data.JsonReader({})
+        });
+
+        this.calendarView = new Tine.Calendar.DaysView({
+            store: this.store,
+            startDate: new Date(),
+            numOfDays: 7,
+            readOnly: true
+        });
+
+        this.calendarView.getSelectionModel().on('selectionchange', this.onViewSelectionChange, this);
+        this.store.on('beforeload', this.onStoreBeforeload, this);
+        this.store.on('load', this.onStoreLoad, this);
+
+        this.detailsPanel = new Tine.Calendar.EventDetailsPanel();
+
+        this.freeTimeSlots = [];
+
+        this.tbar = [{
+            text: i18n._('Back'),
+            minWidth: 70,
+            ref: '../buttonBack',
+            iconCls: 'action_previous',
+            scope: this,
+            disabled: true,
+            handler: this.onButtonBack
+        }, {
+            text: i18n._('Next'),
+            minWidth: 70,
+            ref: '../buttonNext',
+            iconCls: 'action_next',
+            scope: this,
+            handler: this.onButtonNext
+        }, '-', {
+            text: i18n._('Options'),
+            minWidth: 70,
+            ref: '../buttonOptions',
+            iconCls: 'action_options',
+            scope: this,
+            handler: this.onButtonOptions
+        }];
+
+        this.fbar = ['->', {
+            text: i18n._('Cancel'),
+            minWidth: 70,
+            ref: '../buttonCancel',
+            scope: this,
+            handler: this.onButtonCancel,
+            iconCls: 'action_cancel'
+        }, {
+            text: i18n._('Ok'),
+            minWidth: 70,
+            ref: '../buttonApply',
+            scope: this,
+            handler: this.onButtonApply,
+            iconCls: 'action_saveAndClose'
+        }];
+
+        this.items = [{
+            layout: 'border',
+            border: false,
+            items: [{
+                region: 'center',
+                layout: 'fit',
+                border: false,
+                items: this.calendarView
+            }, {
+                region: 'south',
+                border: false,
+                collapsible: true,
+                collapseMode: 'mini',
+                header: false,
+                split: true,
+                layout: 'fit',
+                height: this.detailsPanel.defaultHeight ? this.detailsPanel.defaultHeight : 125,
+                items: this.detailsPanel
+            }]
+        }];
+
+        Tine.Calendar.FreeTimeSearchDialog.superclass.initComponent.call(this);
+
+        this.constraints = Ext.state.Manager.get(this.optionsStateId, Tine.Calendar.EventFinderOptionsDialog.prototype.defaultOptions);
+        this.doFreeTimeSearch();
+    },
+
+    afterRender: function() {
+        Tine.Calendar.FreeTimeSearchDialog.superclass.afterRender.call(this);
+
+        this.loadMask = new Ext.LoadMask(this.getEl(), {msg: this.app.i18n._("Searching for Free Time...")});
+        this.loadMask.show.defer(100, this.loadMask);
+    },
+
+    applyOptions: function(constraints) {
+        // nothing to do
+        if (Ext.encode(this.constraints) == Ext.encode(constraints)) return;
+
+        this.constraints = constraints;
+        this.doFreeTimeSearch()
+    },
+
+    doFreeTimeSearch: function(from) {
+        if (this.loadMask) {
+            this.loadMask.show();
+        }
+
+        var _ = window.lodash,
+            datePart = this.record.get('dtstart').add(Date.DAY, -7).format('Y-m-d '),
+            groupedConstraints = _.groupBy(_.filter(this.constraints, {active: true}), function(constraint) {
+                return constraint.period.from + '-' + constraint.period.until;
+            }),
+            constraints = _.reduce(groupedConstraints, function(result, constraintsGroup) {
+                return result.concat({
+                    dtstart: datePart + constraintsGroup[0].period.from,
+                    dtend: datePart + constraintsGroup[0].period.until,
+                    rrule: 'FREQ=WEEKLY;INTERVAL=1;BYDAY=' + _.map(constraintsGroup, 'id').join(',')
+                });
+            }, []),
+            options = {
+                from: from ? from : this.record.get('dtstart'),
+                constraints: constraints
+            };
+
+        Tine.Calendar.searchFreeTime(this.record.data, options, this.onFreeTimeData, this);
+    },
+
+    onFreeTimeData: function(result) {
+        if (result.timeSearchStopped) {
+            return this.onFreeTimeSearchTimeout(result);
+        }
+
+        var _ = window.lodash,
+            timing = _.get(result, 'results[0]'),
+            dtStart = new Date(timing.dtstart),
+            from = dtStart.clearTime(true).add(Date.DAY, -1 * dtStart.getDay())
+                .add(Date.DAY, Ext.DatePicker.prototype.startDay - (dtStart.getDay() == 0 ? 7 : 0)),
+            dtEnd = new Date(timing.dtend),
+            until = from.add(Date.DAY, 7),
+            period = {from: from, until: until},
+            filterHash = Ext.encode(this.store.baseParams.filter);
+
+        this.store.remove(this.record);
+        this.record.set('dtstart', dtStart);
+        this.record.set('dtend', dtEnd);
+
+        this.freeTimeSlot = result;
+
+        // calculate period start
+        if (Ext.DatePicker.prototype.startDay) {
+            from = from.add(Date.DAY, Ext.DatePicker.prototype.startDay - (dtStart.getDay() == 0 ? 7 : 0));
+        }
+
+        this.store.baseParams.filter = [
+            {field: 'period', operator: 'within', value: period},
+            {field: 'attender', operator: 'in', value: this.record.get('attendee')},
+            {field: 'attender_status', operator: 'notin', value: 'DECLINED'}
+        ];
+
+        if (this.record.get('id')) {
+            this.store.baseParams.filter.push({field: 'id', operator: 'not', value: this.record.get('id')});
+        }
+
+        if (filterHash == Ext.encode(this.store.baseParams.filter)) {
+            this.onStoreLoad();
+        } else {
+            this.calendarView.updatePeriod(period);
+            this.store.load();
+        }
+    },
+
+    onFreeTimeSearchTimeout: function(result) {
+        var _ = window.lodash,
+            dtStopped = new Date(_.get(result, 'timeSearchStopped')),
+            stoppedString = Tine.Tinebase.common.dateRenderer(dtStopped);
+
+        Tine.widgets.dialog.MultiOptionsDialog.openWindow({
+            title: this.app.i18n._('No Free Time found'),
+            questionText: String.format(this.app.i18n._('No free timeslot could be found Until {0}. Do you want to continue?'), '<b>' + stoppedString + '</b>') + '<br><br>',
+            height: 170,
+            scope: this,
+            options: [
+                {text: this.app.i18n._('Continue'), name: 'continue'},
+                {text: this.app.i18n._('Give up'), name: 'giveUp'}
+            ],
+
+            handler: function(option) {
+                switch(option) {
+                    case 'giveUp':
+                        this.onButtonCancel();
+                        break;
+                    case 'continue':
+                        this.doFreeTimeSearch(new Date(dtStopped));
+                        break;
+                }
+            }
+        });
+    },
+
+    onStoreBeforeload: function() {
+        if (this.loadMask) {
+            this.loadMask.show();
+        }
+    },
+
+    onStoreLoad: function() {
+        this.store.each(function(event) {
+            if (event.ui) {
+                event.ui.setOpacity(0.5, 0);
+            }
+        }, this);
+
+        this.store.add([this.record]);
+
+        this.record.ui.setOpacity(1, 0);
+
+        // @TODO check if event is visible and skip scrolling -> defered scrolling looks ugly
+        // this.calendarView.scrollTo.defer(500, this.calendarView, [this.record.get('dtstart')]);
+
+        this.loadMask.hide();
+    },
+
+    onViewSelectionChange: function(sm, selections) {
+        this.detailsPanel.onDetailsUpdate(sm);
+    },
+
+    onButtonBack: function() {
+        this.onFreeTimeData(this.freeTimeSlots.pop());
+        this.buttonBack.setDisabled(!this.freeTimeSlots.length);
+    },
+
+    onButtonNext: function() {
+        this.freeTimeSlots.push(this.freeTimeSlot);
+
+        this.doFreeTimeSearch(this.record.get('dtend'));
+        this.buttonBack.enable();
+    },
+
+    onButtonOptions: function() {
+        Tine.Calendar.EventFinderOptionsDialog.openWindow({
+            titleText: this.app.i18n._('Free Time Search Options'),
+            stateId: this.optionsStateId,
+            recordId: this.record.id,
+            listeners: {
+                scope: this,
+                apply: this.onOptionsApply
+            }
+        });
+    },
+
+    onOptionsApply: function(dialog, options) {
+        this.applyOptions(Ext.decode(options));
+    },
+
+    onButtonCancel: function() {
+        this.fireEvent('cancel');
+        this.purgeListeners();
+        this.window.close();
+    },
+
+    onButtonApply: function() {
+        this.fireEvent('apply', this, Ext.encode(this.record.data));
+        this.purgeListeners();
+        this.window.close();
+    }
+});
+
+/**
+ * Opens a new free time serach dialog window
+ *
+ * @return {Ext.ux.Window}
+ */
+Tine.Calendar.FreeTimeSearchDialog.openWindow = function (config) {
+    var _ = window.lodash;
+
+    if (! _.isString(config.record)) {
+        config.record = Ext.encode(config.record.data);
+    }
+
+    return Tine.WindowFactory.getWindow({
+        width: 1024,
+        height: 768,
+        name: Tine.Calendar.FreeTimeSearchDialog.prototype.windowNamePrefix + _.get(config, 'record.id', 0),
+        contentPanelConstructor: 'Tine.Calendar.FreeTimeSearchDialog',
+        contentPanelConstructorConfig: config
+    });
+};
\ No newline at end of file
index d13264b..ddb8df6 100644 (file)
@@ -192,6 +192,7 @@ Tine.Calendar.Model.Event.getDefaultData = function() {
     }
 
     var data = {
+        id: 'new-' + Ext.id(),
         summary: '',
         'class': eventClass,
         dtstart: dtstart,
index 529bc14..0bd4ba5 100644 (file)
@@ -84,6 +84,7 @@ class Tinebase_Server_WebDAV extends Tinebase_Server_Abstract implements Tinebas
         
         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
             self::$_server->debugExceptions = true;
+            Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " headers: " . print_r(self::$_server->httpRequest->getHeaders(), true));
             $contentType = self::$_server->httpRequest->getHeader('Content-Type');
             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " requestContentType: " . $contentType);
             
index 3a477d7..e8565a9 100644 (file)
@@ -446,6 +446,16 @@ html, body {
     background-image:url(../../images/oxygen/32x32/mimetypes/text-csv.png) !important;
 }
 
+.action_options {
+    background-image:url(../../images/oxygen/16x16/actions/gear.png) !important;
+}
+.x-btn-medium .action_options {
+    background-image:url(../../images/oxygen/22x22/actions/gear.png) !important;
+}
+.x-btn-large .action_options {
+    background-image:url(../../images/oxygen/32x32/actions/gear.png) !important;
+}
+
 .tinebase-action-debug-console {
     background-image:url(../../images/oxygen/16x16/apps/utilities-terminal.png) !important;
 }
index 7200957..247e9d9 100644 (file)
     padding-left: 5px !important;
 }
 
-.tw-editdialog .x-form-item .x-form-display-field {
-    border: 1px solid #b5b8c8;
-    padding: 3px;
-    height: 11px;
-    background-color:#fff;
-    background-image:url(../../../../themes/tine20/resources/images/tine20/form/text-bg.gif);
-}
+/* NOTE: this makes ugly details panel in edit dialogs
+.tw-editdialog .x-form-item .x-form-display-field {*/
+    /*border: 1px solid #b5b8c8;*/
+    /*padding: 3px;*/
+    /*height: 11px;*/
+    /*background-color:#fff;*/
+    /*background-image:url(../../../../themes/tine20/resources/images/tine20/form/text-bg.gif);*/
+/*}*/
 
 .x-ux-displayfield-text {
     background-image: none;