From 30bcd5ff8cce698344d6cbba2146fb39c6436224 Mon Sep 17 00:00:00 2001 From: Jens Kraemer Date: Sat, 25 Apr 2020 17:09:38 +0800 Subject: [PATCH] show currently running timer in window title, menu item --- .../stopwatch_timers_controller.rb | 14 +++- .../hooks/_layouts_base_body_bottom.html.erb | 10 ++- app/views/stopwatch_timers/start.js.erb | 8 +- assets/javascripts/stopwatch.js | 79 ++++++++++++++++--- init.rb | 2 +- lib/stopwatch/timer.rb | 45 ++++++++++- test/unit/timer_test.rb | 10 +++ 7 files changed, 146 insertions(+), 22 deletions(-) diff --git a/app/controllers/stopwatch_timers_controller.rb b/app/controllers/stopwatch_timers_controller.rb index 1cd8c37..84f0989 100644 --- a/app/controllers/stopwatch_timers_controller.rb +++ b/app/controllers/stopwatch_timers_controller.rb @@ -1,7 +1,7 @@ # weiter # # - update time entry row after context menu stop (reflect saved time) -# - show current time in menu item / window title +# # - remember last activity, preselect that in 'new' form # - same for project, unless we are in a project context # - focus first field that needs an action, depending on above @@ -14,6 +14,7 @@ class StopwatchTimersController < ApplicationController before_action :authorize_log_time, only: %i(new create start stop current) before_action :find_time_entry, only: %i(edit update start stop) before_action :authorize_edit_time, only: %i(edit update) + before_action :find_timer, only: %i(new edit current) def new @time_entry = new_time_entry @@ -55,7 +56,7 @@ class StopwatchTimersController < ApplicationController end def stop - r = Stopwatch::StopTimer.new().call + r = Stopwatch::StopTimer.new.call unless r.success? logger.error "unable to stop timer" end @@ -64,14 +65,19 @@ class StopwatchTimersController < ApplicationController end def current - timer = Stopwatch::Timer.new User.current - render json: timer.to_json + render json: @timer.to_json end private + def find_timer + @timer = Stopwatch::Timer.new User.current + @timer.update + end + def find_time_entry @time_entry = time_entries.find params[:id] + end def load_todays_entries diff --git a/app/views/stopwatch/hooks/_layouts_base_body_bottom.html.erb b/app/views/stopwatch/hooks/_layouts_base_body_bottom.html.erb index 43e4dfe..ac4539c 100644 --- a/app/views/stopwatch/hooks/_layouts_base_body_bottom.html.erb +++ b/app/views/stopwatch/hooks/_layouts_base_body_bottom.html.erb @@ -1,3 +1,11 @@ <%= javascript_tag do %> - Stopwatch.currentTimerUrl = '<%= j current_stopwatch_timers_url %>'; + window.stopwatch = window.initStopwatch({ + currentTimerUrl: '<%= j current_stopwatch_timers_url %>', + hourFormat: '<%= j format_hours 0.0 %>' + }); + <% if User.current.logged? %> + window.stopwatch.highlightRunningTimer( + <%= raw Stopwatch::Timer.new(User.current).to_json %> + ) + <% end %> <% end %> diff --git a/app/views/stopwatch_timers/start.js.erb b/app/views/stopwatch_timers/start.js.erb index e58756a..8014894 100644 --- a/app/views/stopwatch_timers/start.js.erb +++ b/app/views/stopwatch_timers/start.js.erb @@ -5,5 +5,11 @@ $('#ajax-modal').html('<%= j render partial: 'new' %>'); showModal('ajax-modal', '700px'); <% end %> -Stopwatch.highlightTimer('<%= @started_time_entry.id if @started_time_entry %>'); + +<% if @started_time_entry %> + window.stopwatch.timerStarted('<%= @started_time_entry.id %>', + '<%= j format_hours @started_time_entry.hours %>'); +<% else %> + window.stopwatch.timerStopped(); +<% end %> diff --git a/assets/javascripts/stopwatch.js b/assets/javascripts/stopwatch.js index 470f34c..98fd184 100644 --- a/assets/javascripts/stopwatch.js +++ b/assets/javascripts/stopwatch.js @@ -1,13 +1,12 @@ -window.Stopwatch = { - highlightRunningTimer: function(){ - $.get(Stopwatch.currentTimerUrl, function(data){ - if (data.running && data.time_entry_id) { - Stopwatch.highlightTimer(data.time_entry_id); - } - }); - }, +window.initStopwatch = function(config){ + var currentTimerUrl = config.currentTimerUrl; + var hourFormat = config.hourFormat; - highlightTimer: function(id){ + var hoursRe = hourFormat.replace(/0+/g, '\\d+').replace(/\./g, '\\.'); + var titleRegexp = new RegExp('^(' + hoursRe + ' - )?(.*)$'); + var menuItemRegexp = new RegExp('^(.*?)( \\(' + hoursRe + '\\))?$') + + var highlightTimer = function(id, timeHtml){ $('table.time-entries tr.time-entry.running').removeClass('running'); if(id && id != ''){ $('table.time-entries tr.time-entry').each(function(idx, el){ @@ -15,10 +14,68 @@ window.Stopwatch = { var trId = tr.attr('id'); if (trId && trId == 'time-entry-'+id) { tr.addClass('running'); + if (timeHtml) { + tr.find('td.hours').html(timeHtml) + } } }); } - } + }; + + var updateSpentTime = function(entryId, spentTime){ + // update window title + var s = '' + if (spentTime){ + s = spentTime + ' - '; + } + var match = titleRegexp.exec(document.title); + document.title = s + match[2]; + + // update menu item + match = menuItemRegexp.exec($('a#stopwatch-menu').text()); + if (spentTime){ + s = ' (' + spentTime + ')'; + } else { + s = ''; + } + $('a#stopwatch-menu').text(match[1] + s); + }; + + var highlightRunningTimer = function(data){ + if (data && data.running && data.time_entry_id) { + highlightTimer(data.time_entry_id, data.html_time_spent); + updateSpentTime(data.time_entry_id, data.time_spent); + window.setTimeout(function(){ + fetchCurrentTimer(highlightRunningTimer); + }, 60000); + } else { + highlightTimer(); + updateSpentTime(); + } + }; + + var fetchCurrentTimer = function(successHandler){ + $.ajax(currentTimerUrl, { + type: 'GET', + contentType: 'application/octet-stream', // suppress the ajax-indicator that's bound to ajax events in application.js + success: successHandler + }); + }; + + //$(document).on('ready', fetchCurrentTimer(highlightRunningTimer)); + + return({ + highlightRunningTimer: highlightRunningTimer, + timerStopped: function(){ + highlightRunningTimer(); + }, + timerStarted: function(entryId, spentTime){ + highlightRunningTimer({ + running: true, + time_entry_id: entryId, + time_spent: spentTime + }); + } + }); }; -$(document).on('ready', Stopwatch.highlightRunningTimer); diff --git a/init.rb b/init.rb index f445600..faa91a8 100644 --- a/init.rb +++ b/init.rb @@ -14,7 +14,7 @@ Redmine::Plugin.register :stopwatch do menu :account_menu, :stopwatch, :new_stopwatch_timer_path, caption: :button_log_time, - html: { method: :get, id: 'stopwatch-toggle', 'data-remote': true }, + html: { method: :get, id: 'stopwatch-menu', 'data-remote': true }, before: :my_account, if: ->(*_){ User.current.logged? and User.current.allowed_to?(:log_time, nil, global: true) } end diff --git a/lib/stopwatch/timer.rb b/lib/stopwatch/timer.rb index e5eacc1..ae9d0ab 100644 --- a/lib/stopwatch/timer.rb +++ b/lib/stopwatch/timer.rb @@ -2,6 +2,24 @@ require 'json' module Stopwatch class Timer + + class HourFormatter + include ApplicationHelper + + def initialize(s) + @s = s + end + + def html_hours + super format_hours + end + + def format_hours + super @s + end + end + + attr_reader :user def initialize(user) @@ -20,18 +38,33 @@ module Stopwatch end def stop - if hours = runtime_hours and - time_entry = TimeEntry.find_by_id(time_entry_id) + update stop: true + end - time_entry.update_column :hours, time_entry.hours + hours + # saves currently logged time to the time entry and resets the started_at + # timestamp to either nil (stop: true) or current time. + def update(stop: false) + if hours = runtime_hours and entry = self.time_entry + time_entry.update_column :hours, entry.hours + hours end - data[:started_at] = nil + data[:started_at] = stop || !running? ? nil : Time.now.to_i save end + def time_spent + h = runtime_hours || 0 + if e = time_entry + h += e.hours + end + h + end + def to_json + formatter = HourFormatter.new(time_spent) { time_entry_id: time_entry_id, + time_spent: formatter.format_hours, + html_time_spent: formatter.html_hours, running: running? }.to_json end @@ -45,6 +78,10 @@ module Stopwatch data[:time_entry_id] end + def time_entry + TimeEntry.find_by_id(time_entry_id) + end + private def data diff --git a/test/unit/timer_test.rb b/test/unit/timer_test.rb index f35a667..18bddeb 100644 --- a/test/unit/timer_test.rb +++ b/test/unit/timer_test.rb @@ -33,6 +33,16 @@ class TimerTest < ActiveSupport::TestCase assert_equal hours + 1, @time_entry.hours end + test "should update time entry" do + hours = @time_entry.hours + @timer.start @time_entry + @data[:started_at] = 1.hour.ago.to_i + @timer.update + + @time_entry.reload + assert_equal hours + 1, @time_entry.hours + end + test "should save and restore" do hours = @time_entry.hours @timer.start @time_entry