forked from extern/egroupware
WIP timesheet timer: persistence and opening a new timesheet when stoping the specific timer
This commit is contained in:
parent
d8e54c72c8
commit
0a9526c152
@ -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'});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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');
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -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');
|
||||||
|
@ -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
244
timesheet/src/Events.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user