From 0a9526c152d87043c1e88a6719a899ec6c655933 Mon Sep 17 00:00:00 2001 From: ralf Date: Fri, 30 Sep 2022 19:06:47 +0200 Subject: [PATCH] WIP timesheet timer: persistence and opening a new timesheet when stoping the specific timer --- api/js/jsapi/egw_timer.js | 7 +- timesheet/inc/class.timesheet_bo.inc.php | 10 - timesheet/inc/class.timesheet_hooks.inc.php | 3 +- timesheet/inc/class.timesheet_ui.inc.php | 34 ++- timesheet/src/Events.php | 244 ++++++++++++++++++++ timesheet/templates/default/edit.xet | 23 ++ 6 files changed, 308 insertions(+), 13 deletions(-) create mode 100644 timesheet/src/Events.php 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('
0:00
', '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 @@ +