From c2fea85c2a125066e5458410289d486ed410c4c7 Mon Sep 17 00:00:00 2001 From: ralf Date: Fri, 14 Oct 2022 21:59:01 +0200 Subject: [PATCH] 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 --- api/js/jsapi/egw_timer.js | 122 +++++++++++++++++++++-- pixelegg/css/mobile.css | 5 + pixelegg/css/monochrome.css | 10 +- pixelegg/css/pixelegg.css | 5 + pixelegg/less/layout_raster_buttons.less | 5 + pixelegg/mobile/fw_mobile.css | 5 + timesheet/src/Events.php | 54 ++++++++-- timesheet/templates/default/timer.xet | 22 +++- 8 files changed, 205 insertions(+), 23 deletions(-) diff --git a/api/js/jsapi/egw_timer.js b/api/js/jsapi/egw_timer.js index c8170555c7..a2bf15a910 100644 --- a/api/js/jsapi/egw_timer.js +++ b/api/js/jsapi/egw_timer.js @@ -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?'); } }); diff --git a/pixelegg/css/mobile.css b/pixelegg/css/mobile.css index a9c0bd8621..ad72410ebb 100644 --- a/pixelegg/css/mobile.css +++ b/pixelegg/css/mobile.css @@ -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; diff --git a/pixelegg/css/monochrome.css b/pixelegg/css/monochrome.css index f1788c95ae..84662c922e 100644 --- a/pixelegg/css/monochrome.css +++ b/pixelegg/css/monochrome.css @@ -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; diff --git a/pixelegg/css/pixelegg.css b/pixelegg/css/pixelegg.css index 8bfa73a5ec..33b5c35bde 100644 --- a/pixelegg/css/pixelegg.css +++ b/pixelegg/css/pixelegg.css @@ -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; diff --git a/pixelegg/less/layout_raster_buttons.less b/pixelegg/less/layout_raster_buttons.less index 142b190e02..fce28b85b0 100644 --- a/pixelegg/less/layout_raster_buttons.less +++ b/pixelegg/less/layout_raster_buttons.less @@ -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; diff --git a/pixelegg/mobile/fw_mobile.css b/pixelegg/mobile/fw_mobile.css index 56b39a4050..84e24f7a0c 100644 --- a/pixelegg/mobile/fw_mobile.css +++ b/pixelegg/mobile/fw_mobile.css @@ -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; diff --git a/timesheet/src/Events.php b/timesheet/src/Events.php index dbe2550b6f..28c7d005d6 100644 --- a/timesheet/src/Events.php +++ b/timesheet/src/Events.php @@ -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']) diff --git a/timesheet/templates/default/timer.xet b/timesheet/templates/default/timer.xet index 11a3cfe8ce..f0a6d2a3d7 100644 --- a/timesheet/templates/default/timer.xet +++ b/timesheet/templates/default/timer.xet @@ -1,6 +1,9 @@ +