WIP timesheet timer: persistence and opening a new timesheet when stoping the specific timer

This commit is contained in:
ralf 2022-09-30 19:06:47 +02:00
parent d8e54c72c8
commit 0a9526c152
6 changed files with 308 additions and 13 deletions

View File

@ -118,7 +118,12 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
break; break;
} }
// persist state // 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'});
}
});
} }
/** /**

View File

@ -1041,14 +1041,4 @@ class timesheet_bo extends Api\Storage
} }
return parent::data2db($intern ? null : $data); // important to use null, if $intern! 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');
}
} }

View File

@ -15,6 +15,7 @@ use EGroupware\Api\Link;
use EGroupware\Api\Framework; use EGroupware\Api\Framework;
use EGroupware\Api\Egw; use EGroupware\Api\Egw;
use EGroupware\Api\Acl; use EGroupware\Api\Acl;
use EGroupware\Timesheet\Events;
if (!defined('TIMESHEET_APP')) if (!defined('TIMESHEET_APP'))
{ {
@ -216,7 +217,7 @@ class timesheet_hooks
public static function add_timer($data) public static function add_timer($data)
{ {
$state = timesheet_bo::timerState(); $state = Events::timerState();
$GLOBALS['egw']->framework->_add_topmenu_info_item('<div id="topmenu_timer" title="'. $GLOBALS['egw']->framework->_add_topmenu_info_item('<div id="topmenu_timer" title="'.
lang('Start & stop timer').'"'. lang('Start & stop timer').'"'.
($state ? ' data-state="'.htmlspecialchars(json_encode($state)).'"' : '').'>0:00</div>', 'timer'); ($state ? ' data-state="'.htmlspecialchars(json_encode($state)).'"' : '').'>0:00</div>', 'timer');

View File

@ -15,6 +15,7 @@ use EGroupware\Api\Link;
use EGroupware\Api\Framework; use EGroupware\Api\Framework;
use EGroupware\Api\Acl; use EGroupware\Api\Acl;
use EGroupware\Api\Etemplate; use EGroupware\Api\Etemplate;
use EGroupware\Timesheet\Events;
/** /**
* User interface object of the TimeSheet * User interface object of the TimeSheet
@ -75,6 +76,7 @@ class timesheet_ui extends timesheet_bo
{ {
$view = true; $view = true;
} }
$this->data['events'] = Events::get($this->data['ts_id']);
} }
else // new entry else // new entry
{ {
@ -92,6 +94,20 @@ class timesheet_ui extends timesheet_bo
{ {
$this->data['pm_id'] = $this->find_pm_id($_REQUEST['ts_project']); $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; $matches = null;
$referer = preg_match('/menuaction=([^&]+)/',$_SERVER['HTTP_REFERER'],$matches) ? $matches[1] : $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']); 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'); Framework::refresh_opener($msg, 'timesheet', $this->data['ts_id'], $content['ts_id'] ? 'edit' : 'add');
if ($button == 'apply') break; if ($button == 'apply') break;
@ -427,6 +450,7 @@ class timesheet_ui extends timesheet_bo
'button[save]' => $view, 'button[save]' => $view,
'button[save_new]' => $view, 'button[save_new]' => $view,
'button[apply]' => $view, 'button[apply]' => $view,
'tabs[events]' => empty($this->data['events']), // hide events tab, if we have none
); );
if ($view) if ($view)
@ -448,6 +472,14 @@ class timesheet_ui extends timesheet_bo
{ {
$edit_grants[$content['ts_owner']] = Api\Accounts::username($content['ts_owner']); $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_owner'] = $edit_grants;
$sel_options['ts_status'] = $this->get_status_labels($only_admin_edit); $sel_options['ts_status'] = $this->get_status_labels($only_admin_edit);
if($this->config_data['history'] && $content['ts_status'] == self::DELETED_STATUS) if($this->config_data['history'] && $content['ts_status'] == self::DELETED_STATUS)
@ -1404,4 +1436,4 @@ class timesheet_ui extends timesheet_bo
} }
} }
} }
} }

244
timesheet/src/Events.php Normal file
View File

@ -0,0 +1,244 @@
<?php
/**
* TimeSheet - Events object
*
* @link http://www.egroupware.org
* @author Ralf Becker <rb@egroupware.org>
* @package timesheet
* @copyright (c) 2022 by Ralf Becker <rb@egroupware.org>
* @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);
}
}

View File

@ -82,6 +82,27 @@
<template id="timesheet.edit.history" template="" lang="" group="0" version="1.7.001"> <template id="timesheet.edit.history" template="" lang="" group="0" version="1.7.001">
<historylog id="history"/> <historylog id="history"/>
</template> </template>
<template id="timesheet.edit.events" template="" lang="" group="0" version="0.1.001">
<grid width="100%" overflow="auto" id="events">
<columns>
<column/>
<column/>
<column/>
</columns>
<rows>
<row class="th">
<description value="Time"/>
<description value="Type"/>
<description value="Duration"/>
</row>
<row>
<date-time id="${row}[tse_time]" readonly="true"/>
<select id="${row}[tse_type]" readonly="true"/>
<date-timeonly id="${row}[total]" readonly="true"/>
</row>
</rows>
</grid>
</template>
<template id="timesheet.edit" template="" lang="" group="0" version="1.9.002"> <template id="timesheet.edit" template="" lang="" group="0" version="1.9.002">
<grid width="100%"> <grid width="100%">
<columns> <columns>
@ -122,6 +143,7 @@
<tab id="links" label="Links"/> <tab id="links" label="Links"/>
<tab id="customfields" label="Custom Fields"/> <tab id="customfields" label="Custom Fields"/>
<tab id="history" label="History"/> <tab id="history" label="History"/>
<tab id="events" label="Events"/>
</tabs> </tabs>
<tabpanels class="dialog-main-timeframe"> <tabpanels class="dialog-main-timeframe">
<template id="timesheet.edit.notes"/> <template id="timesheet.edit.notes"/>
@ -129,6 +151,7 @@
<template id="timesheet.edit.links"/> <template id="timesheet.edit.links"/>
<template id="timesheet.edit.customfields"/> <template id="timesheet.edit.customfields"/>
<template id="timesheet.edit.history"/> <template id="timesheet.edit.history"/>
<template id="timesheet.edit.events"/>
</tabpanels> </tabpanels>
</tabbox> </tabbox>
</row> </row>