mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-28 19:03:14 +01:00
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;
|
||||
}
|
||||
// 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!
|
||||
}
|
||||
|
||||
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\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('<div id="topmenu_timer" title="'.
|
||||
lang('Start & stop 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\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)
|
||||
|
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">
|
||||
<historylog id="history"/>
|
||||
</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">
|
||||
<grid width="100%">
|
||||
<columns>
|
||||
@ -122,6 +143,7 @@
|
||||
<tab id="links" label="Links"/>
|
||||
<tab id="customfields" label="Custom Fields"/>
|
||||
<tab id="history" label="History"/>
|
||||
<tab id="events" label="Events"/>
|
||||
</tabs>
|
||||
<tabpanels class="dialog-main-timeframe">
|
||||
<template id="timesheet.edit.notes"/>
|
||||
@ -129,6 +151,7 @@
|
||||
<template id="timesheet.edit.links"/>
|
||||
<template id="timesheet.edit.customfields"/>
|
||||
<template id="timesheet.edit.history"/>
|
||||
<template id="timesheet.edit.events"/>
|
||||
</tabpanels>
|
||||
</tabbox>
|
||||
</row>
|
||||
|
Loading…
Reference in New Issue
Block a user