WIP timesheet timers: allow to add more time on an existing timesheet with right click "Start timer"

This commit is contained in:
ralf 2022-10-07 19:36:57 +02:00
parent 03fdceb847
commit d8e993dc75
5 changed files with 174 additions and 18 deletions

View File

@ -59,7 +59,8 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
// initiate specific timer, only if running or paused
if (_state.specific?.start || _state.specific?.paused)
{
startTimer(specific, _state.specific?.start, _state.specific?.offset); // to show offset / paused time
startTimer(specific, _state.specific?.start, _state.specific?.offset, _state.specific.app_id); // to show offset / paused time
specific.id = _state.specific.id;
if (_state.specific?.paused)
{
stopTimer(specific, true);
@ -126,10 +127,25 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
break;
}
// persist state
egw.request('timesheet.EGroupware\\Timesheet\\Events.ajax_event', [getState(_action, _time)]).then(() => {
egw.request('timesheet.EGroupware\\Timesheet\\Events.ajax_event', [getState(_action, _time)]).then((tse_id) => {
if (_action.substring(8) === 'specific')
{
specific.id = tse_id;
}
if (_action === 'specific-stop')
{
egw.open(null, 'timesheet', 'add', {events: 'specific'});
let type = 'add';
let extra = {events: 'specific'};
if (specific.app_id && specific.app_id.substring(0, 11) === 'timesheet::')
{
extra.ts_id = specific.app_id.substring(11);
type = 'edit';
}
egw.open(null, 'timesheet', type, extra);
// unset the app_id and the tse_id to not associate the next start with it
specific.app_id = undefined;
specific.id = undefined;
}
});
}
@ -231,8 +247,9 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
* @param object _timer
* @param string|Date|undefined _start to initialise with time different from current time
* @param number|undefined _offset to set an offset
* @param string|undefined _app_id
*/
function startTimer(_timer, _start, _offset)
function startTimer(_timer, _start, _offset, _app_id)
{
// update _timer state object
if (_start)
@ -249,6 +266,7 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
}
_timer.offset = 0; // it's now set in start-time
_timer.paused = false;
_timer.app_id = _app_id;
// update now
update();
@ -311,6 +329,44 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
}
return {
/**
* Start timer for given app and id
*
* @param {Object} _action
* @param {Array} _senders
*/
start_timer(_action, _senders)
{
if (_action.parent.data.nextmatch?.getSelection().all || _senders.length !== 1)
{
egw.message(egw.lang('You must select a single entry!'), 'error');
return;
}
// timer already running, ask user if he wants to associate it with the entry, or cancel
if (specific.start || specific.paused)
{
Et2Dialog.show_dialog((_button) => {
if (_button === Et2Dialog.OK_BUTTON)
{
if (specific.paused)
{
startTimer(specific, undefined, undefined, _senders[0].id);
}
else
{
specific.app_id = _senders[0].id;
egw.request('timesheet.EGroupware\\Timesheet\\Events.ajax_updateAppId', [specific.id, specific.app_id]);
}
}
},
egw.lang('Do you want to associate it with the selected %1 entry?', egw.lang(_senders[0].id.split('::')[0])),
egw.lang('Timer already running or paused'), {},
Et2Dialog.BUTTONS_OK_CANCEL, Et2Dialog.QUESTION_MESSAGE, undefined, egw);
return;
}
startTimer(specific, undefined, undefined, _senders[0].id);
},
/**
* Create timer in top-menu
*

View File

@ -82,27 +82,57 @@ class timesheet_ui extends timesheet_bo
{
$this->data = array(
'ts_start' => $this->today,
'start_time' => '', // force empty start-time
'end_time' => Api\DateTime::to($this->now,'H:i'),
'start_time' => '', // force empty start-time
'end_time' => Api\DateTime::to($this->now, 'H:i'),
'ts_owner' => $GLOBALS['egw_info']['user']['account_id'],
'cat_id' => (int) $_REQUEST['cat_id'],
'ts_status'=> $GLOBALS['egw_info']['user']['preferences']['timesheet']['predefined_status'],
'cat_id' => (int)$_REQUEST['cat_id'],
'ts_status' => $GLOBALS['egw_info']['user']['preferences']['timesheet']['predefined_status'],
'ts_project' => $_REQUEST['ts_project'],
'ts_title_blur' => $_REQUEST['ts_project'],
'events' => [],
);
if(!is_numeric($_REQUEST['ts_project']))
if (!is_numeric($_REQUEST['ts_project']))
{
$this->data['pm_id'] = $this->find_pm_id($_REQUEST['ts_project']);
}
if (isset($_REQUEST['events']))
}
// are we supposed to add pending events, to a new or an existing timesheet
if (isset($_REQUEST['events']))
{
$pending = Events::getPending($_REQUEST['events'] === 'overall', $time);
$this->data['events'] = array_merge($this->data['events'], array_values($pending));
$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'] = (int)$this->data['ts_duration'] + round($time / 60); // minutes
$this->data['ts_quantity'] = (float)$this->data['ts_quantity'] + $this->data['ts_duration'] / 60.0; // hours
// check if any of the events contains an app::id to link the timesheet to
foreach($pending as $event)
{
$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($event['tse_app']) && $event['tse_app'] !== TIMESHEET_APP && !empty($event['tse_app_id']))
{
// existing timesheets can be directly linked (takes care to not link multiple times)
if ($this->data['ts_id'])
{
Api\Link::link(TIMESHEET_APP, $this->data['ts_id'], $event['tse_app'], $event['tse_app_id']);
}
// new timesheets will be linked on saving (need to check to not add multiple times)
else
{
if (!isset($this->data['link_to']['to_id']))
{
$this->data['link_to']['to_id'] = [];
}
if (!in_array($app_id = [
'to_app' => $event['tse_app'],
'to_id' => $event['tse_app_id'],
], $this->data['link_to']['to_id']))
{
$this->data['link_to']['to_id'][] = $app_id;
}
}
}
}
}
if (!empty($this->data['events']))
@ -288,7 +318,8 @@ 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']))
// associate events with the now stored timesheet (need to run for existing timesheets too, if new events are added!)
if (!empty($content['events']))
{
Events::addToTimesheet($this->data['ts_id'], array_map(static function($event)
{
@ -1117,6 +1148,19 @@ class timesheet_ui extends timesheet_bo
'disableClass' => 'th',
);
}
// if specific timer is NOT disabled, allow to book further time on existing sheets
$config = Api\Config::read(TIMESHEET_APP);
if (!in_array('specific', $config['disable_timer'] ?? []))
{
$actions['timer'] = array(
'icon' => 'timesheet/navbar',
'caption' => 'Start timer',
'onExecute' => 'javaScript:app.timesheet.egw.start_timer',
'allowOnMultiple' => false,
'group' => $group,
'disableClass' => 'th',
);
}
$group++;
$actions += array(
'documents' => timesheet_merge::document_action(

View File

@ -29,6 +29,7 @@ deletes this field timesheet de Dieses Feld löschen
determines the order the fields are displayed timesheet de verändert die Reihenfolge der angezeigten Felder
directory with documents to insert entries timesheet de Ordner mit Dokumenten zum Einfügen.
disable timers timesheet de Zeitnehmer ausschalten
do you want to associate it with the selected %1 entry? common de Wollen Sie ihn mit dem ausgewählten %1 Eintrag verbinden?
each value is a line like <id>[=<label>] timesheet de jeder Wert ist eine Zeile in dem Format <id>[=|label>]
edit status common de Status bearbeiten
edit this entry timesheet de diesen Eintrag bearbeiten
@ -129,6 +130,7 @@ show a quantity sum (eg. to sum up negative overtime) admin de Zeige eine Mengen
skip record timesheet de Eintrag überspringen
start timesheet de Start
start & stop timer common de Zeitnehmer starten & stoppen
start timer timesheet de Zeitnehmer starten
starttime timesheet de Startzeit
starttime has to be before endtime !!! timesheet de Startzeit muss vor der Endzeit liegen !!!
status timesheet de Status
@ -149,6 +151,7 @@ this week timesheet de Diese Woche
this year timesheet de Dieses Jahr
ticket modified by %1 at %2 timesheet de Ticket geändert von %1 am %2
timer common de Zeitnehmer
timer already running or paused common de Zeitnehmer läuft bereits oder ist pausiert
timesheet common de Stundenzettel
timesheet csv export timesheet de Export des Stundenzettels als CSV
timesheet csv import timesheet de Stundenzettels CSV Import

View File

@ -29,6 +29,7 @@ deletes this field timesheet en Deletes this field
determines the order the fields are displayed timesheet en Determines the order the fields are displayed
directory with documents to insert entries timesheet en Directory with documents to insert entries
disable timers timesheet en Disable timers
do you want to associate it with the selected %1 entry? common en Do you want to associate it with the selected %1 entry?
each value is a line like <id>[=<label>] timesheet en Each value is a line like <id>[=<label>]
edit status common en Edit status
edit this entry timesheet en Edit this entry
@ -129,6 +130,7 @@ show a quantity sum (eg. to sum up negative overtime) admin en Show a quantity s
skip record timesheet en Skip record
start timesheet en Start
start & stop timer common en Start & stop timer
start timer timesheet en Start timer
starttime timesheet en Start time
starttime has to be before endtime !!! timesheet en Start time has to be before endtime!
status timesheet en Status
@ -149,6 +151,7 @@ this week timesheet en This week
this year timesheet en This year
ticket modified by %1 at %2 timesheet en Ticket modified by %1 at %2
timer common en Timer
timer already running or paused common en Timer already running or paused
timesheet common en Time Sheet
timesheet csv export timesheet en TimeSheet CSV export
timesheet csv import timesheet en TimeSheet CSV import

View File

@ -75,6 +75,7 @@ class Events extends Api\Storage\Base
* Record/persist timer events from UI
*
* @param array $state
* @return int tse_id of created event
* @throws Api\Exception
* @throws Api\Exception\WrongParameter
*/
@ -92,6 +93,16 @@ class Events extends Api\Storage\Base
$type = ($timer === 'overall' ? self::OVERALL : 0) |
($action === 'start' ? self::START : ($action === 'stop' ? self::STOP : self::PAUSE));
$app = $id = $ts_id = null;
if ($timer === 'specific' && !empty($state['app_id']))
{
list($app, $id) = explode('::', $state['app_id'], 2);
if ($app === self::APP)
{
$ts_id = $id;
}
}
$this->init();
$this->save([
'tse_timestamp' => new Api\DateTime(),
@ -99,6 +110,9 @@ class Events extends Api\Storage\Base
'account_id' => $this->user,
'tse_modifier' => $this->user,
'tse_type' => $type,
'tse_app' => $app,
'tse_app_id' => $id,
'ts_id' => $ts_id,
]);
// create timesheet for stopped working time
@ -113,6 +127,27 @@ class Events extends Api\Storage\Base
Api\Json\Response::get()->message(lang('Error storing working time').': '.$e->getMessage(), 'error');
}
}
return $this->data['tse_id'];
}
/**
* Set app::id on an already started timer
*
* @param int $tse_id id of the running timer-event
* @param string $app_id "app::id" string
* @return void
* @throws Api\Db\Exception\InvalidSql
*/
public function ajax_updateAppId(int $tse_id, string $app_id)
{
if ($tse_id > 0 && !empty($app_id))
{
list($app, $id) = explode('::', $app_id);
$this->db->update(self::TABLE, [
'tse_app' => $app,
'tse_app_id' => $id,
], ['tse_id' => $tse_id], __LINE__, __FILE__, self::APP);
}
}
/**
@ -184,6 +219,21 @@ class Events extends Api\Storage\Base
else
{
self::evaluate($state['specific'], $row);
// if event is associated with an app:id, set it again
if ($row['tse_app'] && $row['tse_app_id'])
{
$state['specific']['app_id'] = $row['tse_app'].'::'.$row['tse_app_id'];
}
// for not stopped events, set the tse_id, so we can associate with an "app:id"
if ($row['tse_type'] & self::STOP)
{
$state['specific']['id'] = null;
}
else
{
$state['specific']['id'] = $row['tse_id'];
}
}
}
// format start-times in UTZ as JavaScript Date() understands