diff --git a/api/js/jsapi/egw_timer.js b/api/js/jsapi/egw_timer.js
index c58a90bb92..eb6be3c207 100644
--- a/api/js/jsapi/egw_timer.js
+++ b/api/js/jsapi/egw_timer.js
@@ -118,7 +118,12 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
break;
}
// persist state
- egw.request('timesheet.timesheet_bo.ajax_event', [getState(_action)])
+ egw.request('timesheet.EGroupware\\Timesheet\\Events.ajax_event', [getState(_action)]).then(() => {
+ if (_action === 'specific-stop')
+ {
+ egw.open(null, 'timesheet', 'add', {events: 'specific'});
+ }
+ });
}
/**
diff --git a/timesheet/inc/class.timesheet_bo.inc.php b/timesheet/inc/class.timesheet_bo.inc.php
index 6cd2e71fde..b1d8c5c8d0 100644
--- a/timesheet/inc/class.timesheet_bo.inc.php
+++ b/timesheet/inc/class.timesheet_bo.inc.php
@@ -1041,14 +1041,4 @@ class timesheet_bo extends Api\Storage
}
return parent::data2db($intern ? null : $data); // important to use null, if $intern!
}
-
- public function ajax_event(array $state)
- {
- Api\Cache::setSession(__CLASS__, 'timer', $state);
- }
-
- public static function timerState()
- {
- return Api\Cache::getSession(__CLASS__, 'timer');
- }
}
\ No newline at end of file
diff --git a/timesheet/inc/class.timesheet_hooks.inc.php b/timesheet/inc/class.timesheet_hooks.inc.php
index c65a2e4b57..fbf1f49802 100644
--- a/timesheet/inc/class.timesheet_hooks.inc.php
+++ b/timesheet/inc/class.timesheet_hooks.inc.php
@@ -15,6 +15,7 @@ use EGroupware\Api\Link;
use EGroupware\Api\Framework;
use EGroupware\Api\Egw;
use EGroupware\Api\Acl;
+use EGroupware\Timesheet\Events;
if (!defined('TIMESHEET_APP'))
{
@@ -216,7 +217,7 @@ class timesheet_hooks
public static function add_timer($data)
{
- $state = timesheet_bo::timerState();
+ $state = Events::timerState();
$GLOBALS['egw']->framework->_add_topmenu_info_item('
', 'timer');
diff --git a/timesheet/inc/class.timesheet_ui.inc.php b/timesheet/inc/class.timesheet_ui.inc.php
index 8c1914a913..a5ec29846f 100644
--- a/timesheet/inc/class.timesheet_ui.inc.php
+++ b/timesheet/inc/class.timesheet_ui.inc.php
@@ -15,6 +15,7 @@ use EGroupware\Api\Link;
use EGroupware\Api\Framework;
use EGroupware\Api\Acl;
use EGroupware\Api\Etemplate;
+use EGroupware\Timesheet\Events;
/**
* User interface object of the TimeSheet
@@ -75,6 +76,7 @@ class timesheet_ui extends timesheet_bo
{
$view = true;
}
+ $this->data['events'] = Events::get($this->data['ts_id']);
}
else // new entry
{
@@ -92,6 +94,20 @@ class timesheet_ui extends timesheet_bo
{
$this->data['pm_id'] = $this->find_pm_id($_REQUEST['ts_project']);
}
+ if (isset($_REQUEST['events']))
+ {
+ $this->data['events'] = array_values(Events::getPending($_REQUEST['events'] === 'overall', $time));
+ $start = $this->data['events'][0]['tse_time'];
+ $this->data['ts_start'] = $start;
+ $this->data['start_time'] = Api\DateTime::server2user($start, 'H:s');
+ $this->data['end_time'] = '';
+ $this->data['ts_duration'] = round($time / 60); // minutes
+ $this->data['ts_quantity'] = $this->data['ts_duration'] / 60.0; // hours
+ }
+ }
+ if (!empty($this->data['events']))
+ {
+ array_unshift($this->data['events'], false);
}
$matches = null;
$referer = preg_match('/menuaction=([^&]+)/',$_SERVER['HTTP_REFERER'],$matches) ? $matches[1] :
@@ -272,6 +288,13 @@ class timesheet_ui extends timesheet_bo
{
Link::link(TIMESHEET_APP,$this->data['ts_id'],$content['link_to']['to_id']);
}
+ if (empty($content['ts_id']) && !empty($content['events']))
+ {
+ Events::addToTimesheet($this->data['ts_id'], array_map(static function($event)
+ {
+ return $event['tse_id'];
+ }, $content['events']));
+ }
}
Framework::refresh_opener($msg, 'timesheet', $this->data['ts_id'], $content['ts_id'] ? 'edit' : 'add');
if ($button == 'apply') break;
@@ -427,6 +450,7 @@ class timesheet_ui extends timesheet_bo
'button[save]' => $view,
'button[save_new]' => $view,
'button[apply]' => $view,
+ 'tabs[events]' => empty($this->data['events']), // hide events tab, if we have none
);
if ($view)
@@ -448,6 +472,14 @@ class timesheet_ui extends timesheet_bo
{
$edit_grants[$content['ts_owner']] = Api\Accounts::username($content['ts_owner']);
}
+ $sel_options['tse_type'] = [
+ Events::START => 'start',
+ Events::STOP => 'stop',
+ Events::PAUSE => 'pause',
+ Events::OVERALL|Events::START => 'start',
+ Events::OVERALL|Events::STOP => 'stop',
+ Events::OVERALL|Events::PAUSE => 'pause',
+ ];
$sel_options['ts_owner'] = $edit_grants;
$sel_options['ts_status'] = $this->get_status_labels($only_admin_edit);
if($this->config_data['history'] && $content['ts_status'] == self::DELETED_STATUS)
@@ -1404,4 +1436,4 @@ class timesheet_ui extends timesheet_bo
}
}
}
-}
+}
\ No newline at end of file
diff --git a/timesheet/src/Events.php b/timesheet/src/Events.php
new file mode 100644
index 0000000000..cbc0aa5b6c
--- /dev/null
+++ b/timesheet/src/Events.php
@@ -0,0 +1,244 @@
+
+ * @package timesheet
+ * @copyright (c) 2022 by Ralf Becker
+ * @license https://opensource.org/licenses/gpl-license.php GPL v2+ - GNU General Public License Version 2 or higher
+ */
+
+namespace EGroupware\Timesheet;
+
+use EGroupware\Api;
+
+/**
+ * Events object of the TimeSheet
+ *
+ * Uses eTemplate's Api\Storage as storage object (Table: egw_timesheet).
+ */
+class Events extends Api\Storage\Base
+{
+ const APP = 'timesheet';
+ const TABLE = 'egw_timesheet_events';
+ /**
+ * @var string[] name of timestamps to convert (not set automatic by setup_table/parent::__construct()!)
+ */
+ public $timestamps = [
+ 'tse_timestamp', 'tse_time', 'tse_modified',
+ ];
+
+ /**
+ * Bitfields for egw_timesheet_events.tse_type
+ */
+ const OVERALL = 16;
+ const START = 1;
+ const STOP = 2;
+ const PAUSE = 4;
+
+ /**
+ * @var Events
+ */
+ private static $instance;
+ protected int $user;
+
+ function __construct()
+ {
+ parent::__construct(self::APP,self::TABLE, null, '', true, 'object');
+
+ if (!isset(self::$instance))
+ {
+ self::$instance = $this;
+ }
+ $this->user = $GLOBALS['egw_info']['user']['account_id'];
+ }
+
+ function __destruct()
+ {
+ if (self::$instance === $this)
+ {
+ self::$instance = null;
+ }
+ }
+
+ protected static function getInstance()
+ {
+ if (!isset(self::$instance))
+ {
+ self::$instance = new self();
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Record/persist timer events from UI
+ *
+ * @param array $state
+ * @throws Api\Exception
+ * @throws Api\Exception\WrongParameter
+ */
+ public function ajax_event(array $state)
+ {
+ list($timer, $action) = explode('-', $state['action']);
+ if (!in_array($timer, ['overall', 'specific']) || !in_array($action, ['start', 'stop', 'pause']))
+ {
+ throw new Api\Exception\WrongParameter("Invalid action '$state[action]'!");
+ }
+ if (empty($state['ts']))
+ {
+ throw new Api\Exception\WrongParameter("Invalid timestamp ('ts') '$state[ts]'!");
+ }
+ $type = ($timer === 'overall' ? self::OVERALL : 0) |
+ ($action === 'start' ? self::START : ($action === 'stop' ? self::STOP : self::PAUSE));
+
+ $this->init();
+ $this->save([
+ 'tse_timestamp' => new Api\DateTime(),
+ 'tse_time' => new Api\DateTime($state['ts']),
+ 'account_id' => $this->user,
+ 'tse_modifier' => $this->user,
+ 'tse_type' => $type,
+ ]);
+ }
+
+ public static function timerState()
+ {
+ $state = [
+ 'overall' => [
+ 'offset' => 0,
+ 'start' => null,
+ 'paused' => false,
+ ],
+ 'specific' => [
+ 'offset' => 0,
+ 'start' => null,
+ 'paused' => false,
+ ],
+ ];
+ foreach(self::getInstance()->search('', false, 'tse_id', '', '', false, 'AND', false, [
+ 'ts_id' => null,
+ 'account_id' => self::getInstance()->user,
+ ]) as $row)
+ {
+ if ($row['tse_type'] & self::OVERALL)
+ {
+ self::evaluate($state['overall'], $row);
+ }
+ else
+ {
+ self::evaluate($state['specific'], $row);
+ }
+ }
+ // format start-times in UTZ as JavaScript Date() understands
+ foreach($state as &$timer)
+ {
+ if (isset($timer['start']))
+ {
+ $timer['start'] = (new Api\DateTime($timer['start'], new \DateTimeZone('UTC')))->format(Api\DateTime::ET2);
+ }
+ }
+ return $state;
+ }
+
+ /**
+ * Evaluate events
+ *
+ * @param array& $timer array with keys 'start', 'offset' and 'paused'
+ * @param array $row
+ * @return void
+ */
+ protected static function evaluate(array &$timer, array $row)
+ {
+ if ($row['tse_type'] & self::START)
+ {
+ $timer['start'] = $row['tse_time'];
+ $timer['paused'] = false;
+ }
+ elseif ($timer['start'])
+ {
+ $timer['offset'] += 1000 * ($row['tse_time']->getTimestamp() - $timer['start']->getTimestamp());
+ $timer['start'] = null;
+ $timer['paused'] = ($row['tse_type'] & self::PAUSE) === self::PAUSE;
+ }
+ else // stop of paused timer
+ {
+ $timer['paused'] = ($row['tse_type'] & self::PAUSE) === self::PAUSE;
+ }
+ }
+
+ /**
+ * Get events of given ts_id or a filter
+ *
+ * Not stopped events-sequences are NOT returned (stopped sequences end with a stop event).
+ *
+ * @param int|array $filter
+ * @param int &$time=null on return time in seconds
+ * @return array[] tse_id => array pairs plus extra key sum (time-sum in seconds)
+ */
+ public static function get($filter, int &$time=null)
+ {
+ if (!is_array($filter))
+ {
+ $filter = ['ts_id' => $filter];
+ }
+ $timer = $init_timer = [
+ 'start' => null,
+ 'offset' => 0,
+ 'paused' => false,
+ ];
+ $time = $open = 0;
+ $events = [];
+ foreach(self::getInstance()->search('', false, 'tse_id', '', '',
+ false, 'AND', false, $filter) as $row)
+ {
+ self::evaluate($timer, $row);
+ ++$open;
+
+ if ($row['tse_type'] & self::STOP)
+ {
+ $row['total'] = $time += $timer['offset'] / 1000;
+ $timer = $init_timer;
+ $open = 0;
+ }
+ $events[$row['tse_id']] = $row;
+ }
+ // remove open / unstopped timer events
+ if ($open)
+ {
+ $events = array_slice($events, 0, -$open, true);
+ }
+ return $events;
+ }
+
+ /**
+ * Get pending (overall) events of (current) user
+ *
+ * Not stopped events-sequences are NOT returned (stopped sequences end with a stop event).
+ *
+ * @param bool $overall
+ * @param int &$time=null on return total time in seconds
+ * @return array[] tse_id => array pairs
+ */
+ public static function getPending($overall=false, int &$time=null)
+ {
+ return self::get([
+ 'ts_id' => null,
+ 'account_id' => self::getInstance()->user,
+ ($overall ? '' : 'NOT ').'(tse_type & '.self::OVERALL.')',
+ ], $time);
+ }
+
+ /**
+ * Add given events to timesheet / set ts_id parameter
+ *
+ * @param int $ts_id
+ * @param int[] $events array of tse_id's
+ * @return Api\ADORecordSet|false|int
+ * @throws Api\Db\Exception\InvalidSql
+ */
+ public static function addToTimesheet(int $ts_id, array $events)
+ {
+ return self::getInstance()->db->update(self::TABLE, ['ts_id' => $ts_id], ['tse_id' => $events], __LINE__, __FILE__, self::APP);
+ }
+}
\ No newline at end of file
diff --git a/timesheet/templates/default/edit.xet b/timesheet/templates/default/edit.xet
index c841548686..9d2407cf12 100644
--- a/timesheet/templates/default/edit.xet
+++ b/timesheet/templates/default/edit.xet
@@ -82,6 +82,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -122,6 +143,7 @@
+
@@ -129,6 +151,7 @@
+