diff --git a/app/controllers/stopwatch_timers_controller.rb b/app/controllers/stopwatch_timers_controller.rb
index 104e439..1cd8c37 100644
--- a/app/controllers/stopwatch_timers_controller.rb
+++ b/app/controllers/stopwatch_timers_controller.rb
@@ -1,12 +1,23 @@
+# 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
+#
class StopwatchTimersController < ApplicationController
helper :timelog
before_action :require_login
before_action :find_optional_data, only: %i(new create)
+ 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)
def new
@time_entry = new_time_entry
- respond_to :js
+ load_todays_entries
end
def create
@@ -23,15 +34,54 @@ class StopwatchTimersController < ApplicationController
end
def edit
-
+ @entries = load_todays_entries #.where.not(id: @time_entry.id)
end
def update
+ # todo update entry
+ if params[:continue]
+ new
+ end
+ end
+ def start
+ r = Stopwatch::StartTimer.new(@time_entry).call
+ if r.success?
+ @started_time_entry = @time_entry
+ else
+ logger.error "unable to start timer: #{r.error}"
+ end
+ new unless params[:context]
+ end
+
+ def stop
+ r = Stopwatch::StopTimer.new().call
+ unless r.success?
+ logger.error "unable to stop timer"
+ end
+ new unless params[:context]
+ render action: 'start'
+ end
+
+ def current
+ timer = Stopwatch::Timer.new User.current
+ render json: timer.to_json
end
private
+ def find_time_entry
+ @time_entry = time_entries.find params[:id]
+ end
+
+ def load_todays_entries
+ @entries = time_entries.where(spent_on: User.current.today).order(created_on: :asc)
+ end
+
+ def time_entries
+ TimeEntry.where(user: User.current)
+ end
+
def new_time_entry
TimeEntry.new(project: @project, issue: @issue,
user: User.current, spent_on: User.current.today)
@@ -48,4 +98,12 @@ class StopwatchTimersController < ApplicationController
render_404
end
+ def authorize_log_time
+ User.current.allowed_to?(:log_time, nil, global: true) or
+ deny_access
+ end
+
+ def authorize_edit_time
+ @time_entry.editable_by?(User.current) or deny_access
+ end
end
diff --git a/app/views/stopwatch/hooks/_layouts_base_body_bottom.html.erb b/app/views/stopwatch/hooks/_layouts_base_body_bottom.html.erb
new file mode 100644
index 0000000..43e4dfe
--- /dev/null
+++ b/app/views/stopwatch/hooks/_layouts_base_body_bottom.html.erb
@@ -0,0 +1,3 @@
+<%= javascript_tag do %>
+ Stopwatch.currentTimerUrl = '<%= j current_stopwatch_timers_url %>';
+<% end %>
diff --git a/app/views/stopwatch/hooks/_layouts_base_html_head.html.erb b/app/views/stopwatch/hooks/_layouts_base_html_head.html.erb
new file mode 100644
index 0000000..5674479
--- /dev/null
+++ b/app/views/stopwatch/hooks/_layouts_base_html_head.html.erb
@@ -0,0 +1,2 @@
+<%= javascript_include_tag "stopwatch", plugin: "stopwatch" %>
+<%= stylesheet_link_tag "stopwatch", plugin: "stopwatch" %>
diff --git a/app/views/stopwatch/hooks/_time_entries_context_menu_start.html.erb b/app/views/stopwatch/hooks/_time_entries_context_menu_start.html.erb
new file mode 100644
index 0000000..2080f94
--- /dev/null
+++ b/app/views/stopwatch/hooks/_time_entries_context_menu_start.html.erb
@@ -0,0 +1,10 @@
+<% if @time_entries.one? %>
+ <% time_entry = @time_entries.first %>
+
+ <% if User.current.is_running_timer? time_entry %>
+ <%= context_menu_link l(:label_stopwatch_stop), stop_stopwatch_timer_path(time_entry, context: '1'), class: 'icon-time', remote: true, method: :put %>
+ <% else %>
+ <%= context_menu_link l(:label_stopwatch_start), start_stopwatch_timer_path(time_entry, context: '1'), class: 'icon-time', remote: true, method: :put %>
+ <% end %>
+
+<% end %>
diff --git a/app/views/stopwatch_timers/_edit.html.erb b/app/views/stopwatch_timers/_edit.html.erb
index 31638b0..11edf50 100644
--- a/app/views/stopwatch_timers/_edit.html.erb
+++ b/app/views/stopwatch_timers/_edit.html.erb
@@ -1,11 +1,14 @@
-<%= l(:label_spent_time) %>
+<%= l(:button_log_time) %> - <%= format_date User.current.today %>
-<%= labelled_form_for @time_entry, url: stopwatch_path, remote: true do |f| %>
- <% @time_entry.hours ||= 0 %>
- <%= render partial: 'timelog/form', locals: { f: f } %>
+<%= render partial: 'entries_list', locals: { entries: @entries } %>
+
+<%= labelled_form_for @time_entry, url: stopwatch_timer_path(@time_entry), method: :put, remote: true do |f| %>
+ <%= hidden_field_tag :time_entry_id, @time_entry.id %>
+ <%= render partial: 'entry_form', locals: { f: f } %>
- <%= submit_tag l(:button_create) %>
- <%= submit_tag l(:button_cancel), name: nil, onclick: "hideModal(this);", type: 'button' %>
+ <%= submit_tag l(:button_save) %>
+ <%= submit_tag t('.button_save_and_continue'), name: 'continue' %>
+ <%#= submit_tag l(:button_cancel), name: nil, onclick: "hideModal(this);", type: 'button' %>
<% end %>
diff --git a/app/views/stopwatch_timers/_entries_list.html.erb b/app/views/stopwatch_timers/_entries_list.html.erb
new file mode 100644
index 0000000..8e94269
--- /dev/null
+++ b/app/views/stopwatch_timers/_entries_list.html.erb
@@ -0,0 +1,31 @@
+<% if entries.any? %>
+
+
+| <%= l(:label_activity) %> |
+<%= l(:label_project) %> |
+<%= l(:field_comments) %> |
+<%= l(:field_hours) %> |
+ |
+
+
+<% for entry in entries %>
+ ">
+ | <%= entry.activity %> |
+ <%= entry.project %> <%= h(' - ') + link_to_issue(entry.issue, truncate: 50) if entry.issue %> |
+
+ <%= html_hours(format_hours(entry.hours)) %> |
+
+ <% if User.current.is_running_timer?(entry) %>
+ <%= link_to t(:label_stopwatch_stop), stop_stopwatch_timer_path(entry), remote: true, class: "icon-only icon-time", method: :put %>
+ <% else %>
+ <%= link_to t(:label_stopwatch_start), start_stopwatch_timer_path(entry), remote: true, class: "icon-only icon-time", method: :put %>
+ <% end %>
+ <%= link_to l(:button_edit), edit_stopwatch_timer_path(entry), remote: true, class: "icon-only icon-edit" if entry.editable_by? User.current %>
+ |
+
+<% end -%>
+
+
+<% end %>
+
+
diff --git a/app/views/stopwatch_timers/_entry_form.html.erb b/app/views/stopwatch_timers/_entry_form.html.erb
new file mode 100644
index 0000000..653fd0c
--- /dev/null
+++ b/app/views/stopwatch_timers/_entry_form.html.erb
@@ -0,0 +1,74 @@
+<%= error_messages_for 'time_entry' %>
+
+
+
+<%= javascript_tag do %>
+ $(document).ready(function(){
+ $('#time_entry_project_id').change(function(){
+ $('#time_entry_issue_id').val('');
+ });
+ $('#time_entry_project_id, #time_entry_issue_id').change(function(){
+ $.ajax({
+ url: '<%= escape_javascript(@time_entry.new_record? ? new_time_entry_path(format: 'js') : edit_time_entry_path(@time_entry, format: 'js')) %>',
+ type: 'post',
+ data: $(this).closest('form').serialize()
+ });
+ });
+ });
+
+ observeAutocompleteField('time_entry_issue_id',
+ function(request, callback) {
+ var url = '<%= j auto_complete_issues_path %>';
+ var data = {
+ term: request.term
+ };
+ var project_id;
+ <% if @time_entry.new_record? && @project %>
+ project_id = '<%= @project.id %>';
+ <% else %>
+ project_id = $('#time_entry_project_id').val();
+ <% end %>
+ if(project_id){
+ data['project_id'] = project_id;
+ } else {
+ data['scope'] = 'all';
+ }
+
+ $.get(url, data, null, 'json')
+ .done(function(data){
+ callback(data);
+ })
+ .fail(function(jqXHR, status, error){
+ callback([]);
+ });
+ },
+ {
+ select: function(event, ui) {
+ $('#time_entry_issue').text('');
+ $('#time_entry_issue_id').val(ui.item.value).change();
+ }
+ }
+ );
+<% end %>
+
diff --git a/app/views/stopwatch_timers/_new.html.erb b/app/views/stopwatch_timers/_new.html.erb
index 4888886..7fc3853 100644
--- a/app/views/stopwatch_timers/_new.html.erb
+++ b/app/views/stopwatch_timers/_new.html.erb
@@ -1,12 +1,14 @@
-<%= l(:button_log_time) %>
+<%= l(:button_log_time) %> - <%= format_date User.current.today %>
-<%= labelled_form_for @time_entry, url: stopwatch_timer_path, method: :post, remote: true do |f| %>
+<%= render partial: 'entries_list', locals: { entries: @entries } %>
+
+<%= labelled_form_for @time_entry, url: stopwatch_timers_path, method: :post, remote: true do |f| %>
<% @time_entry.hours ||= 0 %>
- <%= render partial: 'timelog/form', locals: { f: f } %>
+ <%= render partial: 'entry_form', locals: { f: f } %>
<%= submit_tag l(:button_create) %>
- <%= submit_tag l(:button_cancel), name: nil, onclick: "hideModal(this);", type: 'button' %>
+ <%#= submit_tag l(:button_cancel), name: nil, onclick: "hideModal(this);", type: 'button' %>
<% end %>
diff --git a/app/views/stopwatch_timers/create.js.erb b/app/views/stopwatch_timers/create.js.erb
new file mode 100644
index 0000000..f66581c
--- /dev/null
+++ b/app/views/stopwatch_timers/create.js.erb
@@ -0,0 +1 @@
+hideModal();
diff --git a/app/views/stopwatch_timers/edit.js.erb b/app/views/stopwatch_timers/edit.js.erb
new file mode 100644
index 0000000..09e5caf
--- /dev/null
+++ b/app/views/stopwatch_timers/edit.js.erb
@@ -0,0 +1,4 @@
+hideModal();
+$('#ajax-modal').html('<%= j render partial: 'stopwatch_timers/edit' %>');
+showModal('ajax-modal', '700px');
+
diff --git a/app/views/stopwatch_timers/new.js.erb b/app/views/stopwatch_timers/new.js.erb
index 1ffb4da..e232151 100644
--- a/app/views/stopwatch_timers/new.js.erb
+++ b/app/views/stopwatch_timers/new.js.erb
@@ -1,2 +1,2 @@
$('#ajax-modal').html('<%= j render partial: 'stopwatch_timers/new' %>');
-showModal('ajax-modal', '600px');
+showModal('ajax-modal', '700px');
diff --git a/app/views/stopwatch_timers/start.js.erb b/app/views/stopwatch_timers/start.js.erb
new file mode 100644
index 0000000..e58756a
--- /dev/null
+++ b/app/views/stopwatch_timers/start.js.erb
@@ -0,0 +1,9 @@
+<% if params[:context] %>
+ contextMenuHide();
+<% else %>
+ hideModal();
+ $('#ajax-modal').html('<%= j render partial: 'new' %>');
+ showModal('ajax-modal', '700px');
+<% end %>
+Stopwatch.highlightTimer('<%= @started_time_entry.id if @started_time_entry %>');
+
diff --git a/app/views/stopwatch_timers/update.js.erb b/app/views/stopwatch_timers/update.js.erb
new file mode 100644
index 0000000..38dcbc6
--- /dev/null
+++ b/app/views/stopwatch_timers/update.js.erb
@@ -0,0 +1,5 @@
+hideModal();
+<% if params[:continue] %>
+ $('#ajax-modal').html('<%= j render partial: 'stopwatch_timers/new' %>');
+ showModal('ajax-modal', '600px');
+<% end %>
diff --git a/assets/javascripts/stopwatch.js b/assets/javascripts/stopwatch.js
new file mode 100644
index 0000000..470f34c
--- /dev/null
+++ b/assets/javascripts/stopwatch.js
@@ -0,0 +1,24 @@
+window.Stopwatch = {
+ highlightRunningTimer: function(){
+ $.get(Stopwatch.currentTimerUrl, function(data){
+ if (data.running && data.time_entry_id) {
+ Stopwatch.highlightTimer(data.time_entry_id);
+ }
+ });
+ },
+
+ highlightTimer: function(id){
+ $('table.time-entries tr.time-entry.running').removeClass('running');
+ if(id && id != ''){
+ $('table.time-entries tr.time-entry').each(function(idx, el){
+ var tr = $(el);
+ var trId = tr.attr('id');
+ if (trId && trId == 'time-entry-'+id) {
+ tr.addClass('running');
+ }
+ });
+ }
+ }
+};
+
+$(document).on('ready', Stopwatch.highlightRunningTimer);
diff --git a/assets/stylesheets/stopwatch.css b/assets/stylesheets/stopwatch.css
new file mode 100644
index 0000000..53cc4f2
--- /dev/null
+++ b/assets/stylesheets/stopwatch.css
@@ -0,0 +1,3 @@
+tr.time-entry.running td.hours {
+ color: red;
+}
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 63f1c3e..773d28a 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1 +1,12 @@
en:
+ label_stopwatch_start: Start tracking
+ label_stopwatch_stop: Stop tracking
+ stopwatch_timers:
+ entries_list:
+ button_stop: Stop
+ button_resume: Resume
+ entry_form:
+ legend_new: New entry
+ legend_edit: Edit entry
+ edit:
+ button_save_and_continue: Save and continue
diff --git a/config/routes.rb b/config/routes.rb
index 4b3347f..814cb6f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,2 +1,10 @@
-resource :stopwatch_timer, only: %i(new create edit update)
+resources :stopwatch_timers, only: %i(new create edit update) do
+ collection do
+ get :current
+ end
+ member do
+ put :start
+ put :stop
+ end
+end
diff --git a/lib/stopwatch/hooks.rb b/lib/stopwatch/hooks.rb
index e69de29..e035f2e 100644
--- a/lib/stopwatch/hooks.rb
+++ b/lib/stopwatch/hooks.rb
@@ -0,0 +1,11 @@
+module Stopwatch
+ class Hooks < Redmine::Hook::ViewListener
+ render_on :view_layouts_base_html_head,
+ partial: 'stopwatch/hooks/layouts_base_html_head'
+ render_on :view_layouts_base_body_bottom,
+ partial: 'stopwatch/hooks/layouts_base_body_bottom'
+ render_on :view_time_entries_context_menu_start,
+ partial: 'stopwatch/hooks/time_entries_context_menu_start'
+
+ end
+end
diff --git a/lib/stopwatch/start_timer.rb b/lib/stopwatch/start_timer.rb
index 621bb55..852f05f 100644
--- a/lib/stopwatch/start_timer.rb
+++ b/lib/stopwatch/start_timer.rb
@@ -9,16 +9,20 @@ module Stopwatch
end
def call
- if @time_entry.project && @user.allowed_to?(:log_time, @time_entry.project)
+ if @time_entry.project && !@user.allowed_to?(:log_time, @time_entry.project)
return Result.new(error: :unauthorized)
end
- stop_existing_timer
+ StopTimer.new(user: @user).call
@time_entry.hours = 0 if @time_entry.hours.nil?
+ # we want to start tracking time if this is an existing time entry, or a
+ # new entry with 0 hours was created.
+ # new entries with hours > 0 are just saved as is.
+ start_timer = !@time_entry.new_record? || @time_entry.hours == 0
if @time_entry.save
- start_new_timer
+ start_new_timer if start_timer
return Result.new(success: true)
else
Rails.logger.error("could not save time entry: \n#{@time_entry.errors.inspect}")
@@ -33,12 +37,5 @@ module Stopwatch
timer.start @time_entry
end
- def stop_existing_timer
- timer = Timer.new @user
- if timer.running?
- timer.stop
- end
- end
-
end
end
diff --git a/lib/stopwatch/stop_timer.rb b/lib/stopwatch/stop_timer.rb
new file mode 100644
index 0000000..2afc4bf
--- /dev/null
+++ b/lib/stopwatch/stop_timer.rb
@@ -0,0 +1,20 @@
+module Stopwatch
+ class StopTimer
+
+ Result = ImmutableStruct.new(:success?, :error)
+
+ def initialize(user: User.current)
+ @user = user
+ end
+
+ def call
+ timer = Timer.new @user
+ if timer.running?
+ timer.stop
+ end
+ return Result.new(success: true)
+ end
+
+ end
+end
+
diff --git a/lib/stopwatch/timer.rb b/lib/stopwatch/timer.rb
index e33f61f..e5eacc1 100644
--- a/lib/stopwatch/timer.rb
+++ b/lib/stopwatch/timer.rb
@@ -21,7 +21,7 @@ module Stopwatch
def stop
if hours = runtime_hours and
- time_entry = TimeEntry.find_by_id(data[:time_entry_id])
+ time_entry = TimeEntry.find_by_id(time_entry_id)
time_entry.update_column :hours, time_entry.hours + hours
end
@@ -29,11 +29,22 @@ module Stopwatch
save
end
+ def to_json
+ {
+ time_entry_id: time_entry_id,
+ running: running?
+ }.to_json
+ end
+
def save
user.pref[:current_timer] = data.to_json
user.pref.save
end
+ def time_entry_id
+ data[:time_entry_id]
+ end
+
private
def data
diff --git a/lib/stopwatch/user_patch.rb b/lib/stopwatch/user_patch.rb
index 07cb1b1..c0a5d36 100644
--- a/lib/stopwatch/user_patch.rb
+++ b/lib/stopwatch/user_patch.rb
@@ -7,5 +7,10 @@ module Stopwatch
def timer_running?
Stopwatch::Timer.new(self).running?
end
+
+ def is_running_timer?(time_entry)
+ timer = Stopwatch::Timer.new(self)
+ timer.running? and timer.time_entry_id == time_entry.id
+ end
end
end
diff --git a/test/unit/start_timer_test.rb b/test/unit/start_timer_test.rb
index 0b09f18..1ed2c07 100644
--- a/test/unit/start_timer_test.rb
+++ b/test/unit/start_timer_test.rb
@@ -2,11 +2,13 @@ require_relative '../test_helper'
class StartTimerTest < ActiveSupport::TestCase
fixtures :users, :user_preferences, :time_entries, :projects,
- :roles, :member_roles, :members, :enumerations
+ :roles, :member_roles, :members, :enumerations, :enabled_modules
setup do
@user = User.find 1
- @time_entry = TimeEntry.last
+ @time_entry = TimeEntry.where(user_id: 1).first
+ # so we dont have to load all the issue and related fixtures:
+ @time_entry.update_column :issue_id, nil
end
test "should start timer" do
diff --git a/test/unit/stop_timer_test.rb b/test/unit/stop_timer_test.rb
new file mode 100644
index 0000000..05aad3f
--- /dev/null
+++ b/test/unit/stop_timer_test.rb
@@ -0,0 +1,34 @@
+require_relative '../test_helper'
+
+class StopTimerTest < ActiveSupport::TestCase
+ fixtures :users, :user_preferences, :time_entries, :projects,
+ :roles, :member_roles, :members, :enumerations, :enabled_modules
+
+ setup do
+ @user = User.find 1
+ @time_entry = TimeEntry.where(user_id: 1).first
+ # so we dont have to load all the issue and related fixtures:
+ @time_entry.update_column :issue_id, nil
+ end
+
+ test "should stop and save timer" do
+ hours = @time_entry.hours
+ r = Stopwatch::StartTimer.new(@time_entry, user: @user).call
+ assert r.success?
+ t = Stopwatch::Timer.new(@user)
+ data = t.send(:data)
+ data[:started_at] = 1.hour.ago.to_i
+ t.save
+
+ @time_entry.reload
+ assert_equal hours, @time_entry.hours
+ r = Stopwatch::StopTimer.new(user: @user).call
+ assert r.success?
+ @time_entry.reload
+ assert_equal hours+1, @time_entry.hours
+ end
+
+end
+
+
+