WIP timesheet timers: show time under each button and allow overwriting it via a click on the time

ToDo:
- Stop on paused timer does not behave write, overwriting stop needs testing, as timesheet need to be updated too, because it's already stored
- not checks or min/max values and config on overwrite time
This commit is contained in:
ralf 2022-10-14 21:59:01 +02:00
parent c52ac8fbdc
commit c2fea85c2a
8 changed files with 205 additions and 23 deletions

View File

@ -23,6 +23,11 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
* Specific timer state
*/
let specific = {};
/**
* Disable config with values "overall", "specific" or "overwrite"
* @type {string[]}
*/
let disable = [];
/**
* Timer container in top-menu
* @type {Element}
@ -45,8 +50,11 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
*/
function setState(_state)
{
disable = _state.disable;
// initiate overall timer
startTimer(overall, _state.overall?.start, _state.overall?.offset); // to show offset / paused time
overall.started = _state.overall?.started ? new Date(_state.overall.started) : undefined;
overall.started_id = _state.overall?.started_id;
if (_state.overall?.paused)
{
stopTimer(overall, true);
@ -61,6 +69,8 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
if (_state.specific?.start || _state.specific?.paused)
{
startTimer(specific, _state.specific?.start, _state.specific?.offset, _state.specific.app_id); // to show offset / paused time
specific.started = _state.specific?.started ? new Date(_state.specific.started) : undefined;
specific.started_id = _state.specific?.started_id;
specific.id = _state.specific.id;
if (_state.specific?.paused)
{
@ -140,6 +150,18 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
if (_action.substring(0, 8) === 'specific')
{
specific.id = tse_id;
if (_action.substring(9) === 'start')
{
specific.started_id = tse_id;
}
}
else
{
overall.id = tse_id;
if (_action.substring(9) === 'start')
{
overall.started_id = tse_id;
}
}
if (_action === 'specific-stop')
{
@ -259,13 +281,13 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
*/
function startTimer(_timer, _start, _offset)
{
const time = _start ? new Date(_start) : new Date();
if (_timer.last && time.valueOf() < _timer.last.valueOf())
_timer.started = _start ? new Date(_start) : new Date();
if (_timer.last && _timer.started.valueOf() < _timer.last.valueOf())
{
throw egw.lang('Start-time can not be before last stop- or pause-time %1!', formatUTCTime(_timer.last));
}
// update _timer state object
_timer.last = _timer.start = time;
_timer.start = new Date(_timer.last = _timer.started);
if (_offset || _timer.offset && _timer.paused)
{
@ -394,10 +416,9 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
/**
* Open the timer dialog to start/stop timers
*
* @param {Array} disable
* @param {string} _title default "Start & stop timer"
*/
function timerDialog(disable, _title)
function timerDialog(_title)
{
// Pass egw in the constructor
dialog = new Et2Dialog(egw);
@ -418,6 +439,7 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
Et2Dialog.alert(e, egw.lang('Invalid Input'), Et2Dialog.ERROR_MESSAGE);
}
setButtonState();
updateTimes();
return false;
}
dialog = undefined;
@ -429,7 +451,11 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
],
value: {
content: {
disable: disable.join(':')
disable: disable.join(':'),
times: {
specific: getTimes(specific),
overall: getTimes(overall)
}
},
sel_options: {}
}
@ -444,14 +470,92 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
});
}
/**
* Update times displayed under buttons
*/
function updateTimes()
{
if (!dialog) return;
const times = {
specific: getTimes(specific),
overall: getTimes(overall)
};
// disable not matching / available menu-items
dialog._overlayContentNode.querySelectorAll('et2-date-time-today').forEach(_widget => {
const [,timer, action] = _widget.id.match(/times\[([^\]]+)\]\[([^\]]+)\]/);
_widget.value = times[timer][action];
});
}
/**
* Get start, pause and stop time of timer to display in UI
*
* @param {Object} _timer
* @return {Object} with attributes start, pause, stop
*/
function getTimes(_timer)
{
const started = _timer.started ? new Date(_timer.started.valueOf() - egw.getTimezoneOffset() * 60000) : undefined;
const last = _timer.last ? new Date(_timer.last.valueOf() - egw.getTimezoneOffset() * 60000) : undefined;
return {
start: started,
paused: _timer.paused ? last : undefined,
stop: !_timer.start && !_timer.paused ? last : undefined
};
}
return {
/**
* Change/overwrite time
*
* @param {PointerEvent} _ev
* @param {Et2DateTimeToday} _widget
*/
change_timer: function(_ev, _widget)
{
// if there is no value, or timer overwrite is disabled --> ignore click
if (!_widget?.value || disable.indexOf('overwrite') !== -1) {
return;
}
const [, which, action] = _widget.id.match(/times\[([^\]]+)\]\[([^\]]+)\]/);
const timer = which === 'overall' ? overall : specific;
const tse_id = timer[action === 'start' ? 'started_id' : 'id'];
const time = _widget.value;
const dialog = new Et2Dialog(egw);
// Set attributes. They can be set in any way, but this is convenient.
dialog.transformAttributes({
callback: (_button, _values) => {
if (_button === Et2Dialog.OK_BUTTON)
{
_widget.value = _values.time;
// for start we need to take the (not stored) offset into account
const offset = action === 'start' ? timer.started.valueOf() - timer.start.valueOf() : 0;
timer[action] = new Date((new Date(_values.time)).valueOf() + egw.getTimezoneOffset() * 60000 - offset);
egw.request('timesheet.EGroupware\\Timesheet\\Events.ajax_updateTime',
[tse_id, new Date((new Date(_values.time)).valueOf() + egw.getTimezoneOffset() * 60000)])
}
},
title: egw.lang('Change time'),
template: 'timesheet.timer.change',
buttons: Et2Dialog.BUTTONS_OK_CANCEL,
value: {
content: { time: _widget.value }
}
});
// Add to DOM, dialog will auto-open
document.body.appendChild(dialog);
},
/**
* Start timer for given app and id
*
* @param {Object} _action
* @param {Array} _senders
*/
start_timer(_action, _senders)
start_timer: function(_action, _senders)
{
if (_action.parent.data.nextmatch?.getSelection().all || _senders.length !== 1)
{
@ -503,7 +607,7 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
// bind click handler
timer_container.addEventListener('click', (ev) => {
timerDialog(state.disable);
timerDialog();
});
// check if overall working time is not disabled
@ -534,7 +638,7 @@ egw.extend('timer', egw.MODULE_GLOBAL, function()
// overall timer running for more than 16 hours, ask to stop
else if (overall?.start && (((new Date()).valueOf() - overall.start.valueOf()) / 3600000) >= 16)
{
timerDialog(state.disable, 'Forgot to switch off working time?');
timerDialog('Forgot to switch off working time?');
}
});

View File

@ -5274,6 +5274,11 @@ span.overlayContainer img.overlay {
Created on : 23.07.2014, 13:25:11
Author : stefanreinhardt
*/
et2-date-time-today[id^=_times] {
text-align: center;
color: gray;
cursor: pointer;
}
.timesheet_timer,
#egw_fw_topmenu_info_items #topmenu_info_timer #topmenu_timer {
text-align: center;

View File

@ -4542,7 +4542,7 @@ span.overlayContainer img.overlay {
#egw_fw_main #egw_fw_tabs .egw_fw_ui_tabs_header .egw_fw_ui_tab_header:hover {
background-color: rgba(153, 204, 255, 0.4);
padding-bottom: 0px;
padding-top: 7px;
border-top: 7px solid transparent;
transition: none;
width: -webkit-fill-available;
width: -moz-available;
@ -4581,6 +4581,9 @@ span.overlayContainer img.overlay {
border-right: 1px solid #bfc0bf;
border-bottom: 4px solid white !important;
border-top: 4px solid transparent;
width: -webkit-fill-available;
width: -moz-available;
max-width: fit-content !important;
background-color: #ffffff;
background-image: none !important;
}
@ -5251,6 +5254,11 @@ span.overlayContainer img.overlay {
Created on : 23.07.2014, 13:25:11
Author : stefanreinhardt
*/
et2-date-time-today[id^=_times] {
text-align: center;
color: gray;
cursor: pointer;
}
.timesheet_timer,
#egw_fw_topmenu_info_items #topmenu_info_timer #topmenu_timer {
text-align: center;

View File

@ -5264,6 +5264,11 @@ span.overlayContainer img.overlay {
Created on : 23.07.2014, 13:25:11
Author : stefanreinhardt
*/
et2-date-time-today[id^=_times] {
text-align: center;
color: gray;
cursor: pointer;
}
.timesheet_timer,
#egw_fw_topmenu_info_items #topmenu_info_timer #topmenu_timer {
text-align: center;

View File

@ -18,6 +18,11 @@
@import (reference) "definitions.less";
et2-date-time-today[id^=_times] {
text-align: center;
color: gray;
cursor: pointer;
}
.timesheet_timer, #egw_fw_topmenu_info_items #topmenu_info_timer #topmenu_timer {
text-align: center;
font-size: 20px;

View File

@ -5285,6 +5285,11 @@ span.overlayContainer img.overlay {
Created on : 23.07.2014, 13:25:11
Author : stefanreinhardt
*/
et2-date-time-today[id^=_times] {
text-align: center;
color: gray;
cursor: pointer;
}
.timesheet_timer,
#egw_fw_topmenu_info_items #topmenu_info_timer #topmenu_timer {
text-align: center;

View File

@ -147,7 +147,45 @@ class Events extends Api\Storage\Base
$this->db->update(self::TABLE, [
'tse_app' => $app,
'tse_app_id' => $id,
], ['tse_id' => $tse_id], __LINE__, __FILE__, self::APP);
], [
'tse_id' => $tse_id,
'account_id' => $this->user,
'ts_id IS NULL',
], __LINE__, __FILE__, self::APP);
}
}
/**
* Set tse_time on an already started timer
*
* @param int $tse_id id of the running timer-event
* @param string $time
* @return void
* @throws Api\Db\Exception\InvalidSql
*/
public function ajax_updateTime(int $tse_id, string $time)
{
if ($tse_id > 0 && !empty($time) && ($event = $this->read($tse_id)))
{
$time = new Api\DateTime($time);
$this->db->update(self::TABLE, [
'tse_time' => $time,
], [
'tse_id' => $tse_id,
'account_id' => $this->user,
], __LINE__, __FILE__, self::APP);
// if a stop event is overwritten, we need to adjust the timesheet duration and quantity
if (!empty($event['ts_id']) && ($diff = round(($time->getTimestamp() - $event['tse_time'].getTimestamp()) / 60)))
{
$bo = new \timesheet_bo();
if ($bo->read($event['ts_id']))
{
$bo->data['ts_duration'] += $diff;
$bo->data['ts_quantity'] += $diff / 60;
$bo->save();
}
}
}
}
@ -254,13 +292,12 @@ class Events extends Api\Storage\Base
// format start-times in UTZ as JavaScript Date() understands
foreach($state as &$timer)
{
if (isset($timer['start']))
foreach(['start', 'started', 'last'] as $name)
{
$timer['start'] = (new Api\DateTime($timer['start'], new \DateTimeZone('UTC')))->format(Api\DateTime::ET2);
}
if (isset($timer['last']))
{
$timer['last'] = (new Api\DateTime($timer['last'], new \DateTimeZone('UTC')))->format(Api\DateTime::ET2);
if (isset($timer[$name]))
{
$timer[$name] = (new Api\DateTime($timer[$name], new \DateTimeZone('UTC')))->format(Api\DateTime::ET2);
}
}
}
// send timer configuration to client-side
@ -299,7 +336,8 @@ class Events extends Api\Storage\Base
{
if ($row['tse_type'] & self::START)
{
$timer['start'] = $row['tse_time'];
$timer['start'] = $timer['started'] = $row['tse_time'];
$timer['started_id'] = $row['tse_id'];
$timer['paused'] = false;
}
elseif ($timer['start'])

View File

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2//EN" "http://www.egroupware.org/etemplate2.dtd">
<overlay>
<template id="timesheet.timer.change" template="" lang="" group="0" version="1.7.002">
<et2-date-time id="time" required="true"/>
</template>
<template id="timesheet.timer" template="" lang="" group="0" version="1.7.002">
<grid width="100%" >
<columns>
@ -11,11 +14,6 @@
<column/>
</columns>
<rows>
<row disabled="@disable=/overwrite/">
<description value="Overwrite time" span="2"/>
<et2-date-time id="time" span="all" placeholder="now"
onclick="if (!this.value) this.value = new Date((new Date).valueOf() - egw.getTimezoneOffset() * 60000)"/>
</row>
<row disabled="@disable=/specific/">
<description value="Timer"/>
<old-box id="specific_timer" value="00:00" class="timesheet_timer"/>
@ -23,6 +21,13 @@
<et2-button id="specific[pause]" label="Pause" image="timesheet/pause-orange" disabled="true"/>
<et2-button id="specific[stop]" label="Stop" image="timesheet/stop" disabled="true"/>
</row>
<row disabled="@disable=/specific/">
<description />
<description/>
<et2-date-time-today id="times[specific][start]" onclick="egw.change_timer"/>
<et2-date-time-today id="times[specific][paused]" onclick="egw.change_timer"/>
<et2-date-time-today id="times[specific][stop]" onclick="egw.change_timer"/>
</row>
<row disabled="@disable=/overall/">
<description value="Working time"/>
<old-box id="overall_timer" value="00:00" class="timesheet_timer overall"/>
@ -30,6 +35,13 @@
<et2-button id="overall[pause]" label="Pause" image="timesheet/pause-orange" disabled="true"/>
<et2-button id="overall[stop]" label="Stop" image="timesheet/stop" disabled="true"/>
</row>
<row disabled="@disable=/overall/">
<description />
<description/>
<et2-date-time-today id="times[overall][start]" onclick="egw.change_timer"/>
<et2-date-time-today id="times[overall][paused]" onclick="egw.change_timer"/>
<et2-date-time-today id="times[overall][stop]" onclick="egw.change_timer"/>
</row>
</rows>
</grid>
</template>