diff --git a/api/js/jsapi/egw_timer.js b/api/js/jsapi/egw_timer.js index 4dafc0ede0..5734364055 100644 --- a/api/js/jsapi/egw_timer.js +++ b/api/js/jsapi/egw_timer.js @@ -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 * diff --git a/timesheet/inc/class.timesheet_ui.inc.php b/timesheet/inc/class.timesheet_ui.inc.php index a5ec29846f..bdaff99e3f 100644 --- a/timesheet/inc/class.timesheet_ui.inc.php +++ b/timesheet/inc/class.timesheet_ui.inc.php @@ -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( diff --git a/timesheet/lang/egw_de.lang b/timesheet/lang/egw_de.lang index 74a2453ccd..c9676952dd 100644 --- a/timesheet/lang/egw_de.lang +++ b/timesheet/lang/egw_de.lang @@ -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 [=