diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 85478ef..0393a9f 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -19,6 +19,7 @@ //= require jquery-ui-1.10.3.custom.min //= require bootstrap //= require partials/bootstrap-plugins +//= require partials/loading //= require partials/mask //= require partials/timer //= require partials/timesheet diff --git a/app/assets/javascripts/partials/loading.js.coffee b/app/assets/javascripts/partials/loading.js.coffee new file mode 100644 index 0000000..4a4733a --- /dev/null +++ b/app/assets/javascripts/partials/loading.js.coffee @@ -0,0 +1,20 @@ +class @Loading + + @loading = $('#loading') + @bar = $('#loading .bar') + @loading_timeouts = [] + + @show: -> + @loading.show() + @bar.width('20%') + t = setTimeout((-> Loading.bar.width('40%')), 500) + @loading_timeouts.push(t) + t = setTimeout((-> Loading.bar.width('60%')), 1000) + @loading_timeouts.push(t) + + @hide: -> + $.each @loading_timeouts, (i, t) -> + clearTimeout(t) + @loading_timeouts = [] + @loading.hide() + @bar.width('0%') \ No newline at end of file diff --git a/app/assets/javascripts/partials/timer.js.coffee b/app/assets/javascripts/partials/timer.js.coffee index f61266c..a5171c1 100644 --- a/app/assets/javascripts/partials/timer.js.coffee +++ b/app/assets/javascripts/partials/timer.js.coffee @@ -65,10 +65,10 @@ class TimeWindow minute = (if (minute < 10) then "0" + minute else minute) hour = (if (hour < 10) then "0" + hour else hour) - @updateDuo 0, 1, day - @updateDuo 2, 3, hour - @updateDuo 4, 5, minute - @updateDuo 6, 7, second + #@updateDuo 0, 1, day + @updateDuo 0, 1, hour + @updateDuo 2, 3, minute + #@updateDuo 6, 7, second reset: (time_sec) -> @@ -89,14 +89,14 @@ class TimeWindow $(document).ready (e) -> $.each $("div.stopped"), (i) -> - time_sec = $(this).attr("data-id") + time_sec = $(this).attr("data-duration") timer = new TimeWindow($(this)) timer.reset time_sec timer.stop() $.each $("div.running"), (i) -> - time_sec = $(this).attr("data-id") + time_sec = $(this).attr("data-duration") timer = new TimeWindow($(this)) $(this).data "timer", timer @@ -104,12 +104,14 @@ $(document).ready (e) -> timer.reset(time_sec) timer.start() - $(".stop-button").click -> - clock = $(this).parent().siblings(".running") + $(document).on 'click', "a.stop-button", -> + clock = $(this).parents(".timer-button").siblings(".running") timer = clock.data("timer") + if $.isEmptyObject timer + timer = new TimeWindow($(this).parents('tr').children('div.count-holder')) time = timer.getTime() - clock.removeClass("running").addClass ".stopped" + clock.removeClass("running").addClass "stopped" clock.attr "data-id", time timer.stop() $(this).addClass("none").siblings(".btn-inverse").removeClass "none" \ No newline at end of file diff --git a/app/assets/javascripts/partials/timesheet.js.coffee b/app/assets/javascripts/partials/timesheet.js.coffee index 4b097e6..58438fd 100644 --- a/app/assets/javascripts/partials/timesheet.js.coffee +++ b/app/assets/javascripts/partials/timesheet.js.coffee @@ -6,3 +6,197 @@ jQuery -> $(".icon-calendar").on 'click', -> $('#datepicker').toggle() + + # + # Time entry modal (generic scripts) + # + timeEntryModal = $('#timeEntryModal') + timeEntryForm = timeEntryModal.find('form').first() + + timeEntryForm.on 'ajax:before', -> + if timeEntryModal.data('form-type') == 'create' + timeEntryForm.attr('action', '/time_entries.json') + timeEntryForm.attr('method', 'POST') + else if timeEntryModal.data('form-type') == 'update' + timeEntryForm.attr('action', '/time_entries/' + timeEntryModal.data('entry-id') + '.json') + timeEntryForm.attr('method', 'PUT') + + timeEntryForm.on 'ajax:beforeSend', -> + Loading.show() + + timeEntryForm.on 'ajax:complete', -> + Loading.hide() + timeEntryModal.modal('hide') + + timeEntryForm.on 'ajax:error', (event, data, status) -> + if status == 422 + html_messages = "" + else + html_messages = '

Something wrong has happened

' + + $('#time-entry-form-errors .messages').html(html_messages) + $('#time-entry-form-errors').show() + + timeEntryForm.on 'ajax:success', (event, data) -> + if timeEntryModal.data('form-type') == 'create' + new_entry = create_time_entry(data) + $('#timesheet-table').show() + $('#no-day-entries-alert').hide() + $('tr:not(.hide) .stop-button-container:not(.hide)').children('.stop-button').trigger('click') + $('#timesheet-table tbody tr').eq(data.position).after(new_entry) + else if timeEntryModal.data('form-type') == 'update' + update_time_entry(data) + + # + # Shared by time entries scripts + # + timesheet_table = $('#timesheet-table') + + populate_time_entry_row = (entry_row, time_entry) -> + entry_row.attr('data-entry-id', time_entry.id) + entry_row.attr('data-project-id', time_entry.project.id) + entry_row.attr('data-task-id', time_entry.task.id) + entry_row.attr('data-description', time_entry.description) + entry_row.attr('data-start-time', time_entry.start_time) + entry_row.attr('data-end-time', time_entry.end_time) + entry_row.attr('data-is-billable', time_entry.is_billable) + + if time_entry.is_billable + entry_row.find('.billable').show() + else + entry_row.find('.billable').hide() + + entry_row.find('.project-name').html(time_entry.project.name) + entry_row.find('.task-name').html(time_entry.task.name) + + if !$.isEmptyObject time_entry.description + entry_row.find('.description-devider').show() + description = entry_row.find('.time-entry-description') + description.find('.description-text').html(time_entry.description) + description.show() + + entry_row.find('.start-time').html(time_entry.start_time) + + if !$.isEmptyObject(time_entry.end_time) + entry_row.find('.count-holder').addClass('stopped') + entry_row.find('.end-time').html(time_entry.end_time) + entry_row.find('.stop-button-container').hide() + entry_row.find('.start-button-container').show() + + mins = Math.floor(time_entry.duration / 60) % 60 + hours = Math.floor(time_entry.duration / 3600) + entry_row.find('.count-hours').find('.first-digit').children().html(Math.floor(hours / 10)) + entry_row.find('.count-hours').find('.second-digit').children().html(hours % 10) + entry_row.find('.count-minutes').find('.first-digit').children().html(Math.floor(mins / 10)) + entry_row.find('.count-minutes').find('.second-digit').children().html(mins % 10) + + entry_row.find('.stop-button').attr('href', '/time_entries/' + time_entry.id + '/finish.json') + + # + # Scripts for creating a new time entry + # + $('#new-time-entry-btn').on 'click', -> + timeEntryModal.data('form-type', 'create') + show_create_time_entry_modal() + + show_create_time_entry_modal = -> + $('#time-entry-form-errors').hide() + timeEntryModal.find('.new-time-form').show() + timeEntryModal.find('.edit-time-form').hide() + # reset text fields + fields_to_reset = '#time-entry-description, #time-entry-start-time, #time-entry-end-time' + timeEntryModal.find(fields_to_reset).val('') + # reset checkboxes + timeEntryModal.find(':checkbox').attr('checked', false) + + create_time_entry = (time_data)-> + entry_row = $('#time-entry-sample').clone() + entry_row.removeAttr('id') + entry_row.removeClass('hide') + + populate_time_entry_row(entry_row, time_data) + + delete_time_entry_link = entry_row.find('.delete-time-entry') + delete_time_entry_link.attr('href', '/time_entries/' + time_data.id) + + return entry_row + + # + # Scripts for editing an existing time entry + # + timesheet_table.on 'click', '.edit-time-entry', -> + timeEntryModal.data('form-type', 'update') + + entry_row = $(this).parents('tr') + entry_id = entry_row.attr('data-entry-id') + project = entry_row.attr('data-project-id') + task = entry_row.attr('data-task-id') + description = entry_row.attr('data-description') + start_time = entry_row.attr('data-start-time') + end_time = entry_row.attr('data-end-time') + billable = entry_row.attr('data-is-billable') + + timeEntryModal.find('#time-entry-project').val(project) + timeEntryModal.find('#time-entry-task').val(task) + timeEntryModal.find('#time-entry-description').val(description) + timeEntryModal.find('#time-entry-start-time').val(start_time) + timeEntryModal.find('#time-entry-end-time').val(end_time) + if billable == "true" + timeEntryModal.find('#time-entry-is-billable').attr('checked', billable) + + timeEntryModal.data('entry-id', entry_id) + + show_edit_time_entry_modal() + + show_edit_time_entry_modal = -> + $('#time-entry-form-errors').hide() + timeEntryModal.find('.new-time-form').hide() + timeEntryModal.find('.edit-time-form').show() + + update_time_entry = (time_data) -> + $.each $('#timesheet-table tbody tr'), (i, row) -> + if $(row).data('entry-id') == time_data.id + populate_time_entry_row($(row), time_data) + + # + # Scripts for deleting time entries + # + timesheet_table.on 'ajax:success', '.delete-time-entry', -> + entry_row = $(this).parents('tr') + entry_row.fadeOut 'slow', -> + entry_row.remove() + if $('#timesheet-table tbody tr').not('.hide').size() == 0 + timesheet_table.hide() + $('#no-day-entries-alert').fadeIn() + + # + # Scripts for time entries Start and Stop buttons + # + timesheet_table.on 'click', '.start-button', -> + entry_row = $(this).parents('tr') + hsh = { + time_entry: { + project_id: entry_row.attr('data-project-id'), + task_id: entry_row.attr('data-task-id'), + description: entry_row.attr('data-description'), + is_billable: "false" + } + year: timesheet_table.attr('data-year'), + month: timesheet_table.attr('data-month'), + day: timesheet_table.attr('data-day') + } + $.post('/time_entries.json', hsh).success (data) -> + $('tr:not(.hide) .stop-button-container:not(.hide)').children('.stop-button').trigger('click') + new_entry = create_time_entry(data) + $('#timesheet-table tbody tr').eq(data.position).after(new_entry) + + timesheet_table.on 'ajax:success', '.stop-button', (event, data) -> + $(this).parents('.timer-button').children('.start-button-container').removeClass('hide') + $(this).parents('tr').find('.end-time').html(data.ended_at) + $(this).parent().addClass('hide') + + + diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index 1ba130d..a66c69b 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -22,10 +22,11 @@ @import "jquery-ui-1.10.3.custom.min"; /* Import partials */ +@import "partials/loading"; @import "partials/header"; @import "partials/footer"; @import "partials/main"; @import "partials/timer"; @import "partials/timesheet"; @import "partials/reports"; -@import "partials/time_entry" \ No newline at end of file +@import "partials/time_entry_form"; diff --git a/app/assets/stylesheets/partials/loading.css.scss b/app/assets/stylesheets/partials/loading.css.scss new file mode 100644 index 0000000..ae2ec97 --- /dev/null +++ b/app/assets/stylesheets/partials/loading.css.scss @@ -0,0 +1,15 @@ +#loading { + z-index: 9999; + position: absolute; + width: 100%; + height: 100%; + background: rgb(0,0,0); + background: rgba(0,0,0,0.8); + + .progress { + width: 400px; + margin: auto; + margin-top: 20%; + } + +} \ No newline at end of file diff --git a/app/assets/stylesheets/partials/main.css.scss b/app/assets/stylesheets/partials/main.css.scss index ce58b4b..94b016a 100644 --- a/app/assets/stylesheets/partials/main.css.scss +++ b/app/assets/stylesheets/partials/main.css.scss @@ -50,11 +50,11 @@ blockquote { padding-left: 0px; margin: 0px; border: 0px; } -.started_at { +.start-time { color: #033c73; } -.ended_at { +.end-time { color: #dd5600; } diff --git a/app/assets/stylesheets/partials/time_entry.css.scss b/app/assets/stylesheets/partials/time_entry.css.scss deleted file mode 100644 index 0a849cd..0000000 --- a/app/assets/stylesheets/partials/time_entry.css.scss +++ /dev/null @@ -1,5 +0,0 @@ - -textarea.time_entry_form { - width: 500px; - height: 100px; -} \ No newline at end of file diff --git a/app/assets/stylesheets/partials/time_entry_form.css.scss b/app/assets/stylesheets/partials/time_entry_form.css.scss new file mode 100644 index 0000000..c4dff8f --- /dev/null +++ b/app/assets/stylesheets/partials/time_entry_form.css.scss @@ -0,0 +1,7 @@ +#timeEntryModal { + + #time-entry-description { + height: 100px; + } + +} diff --git a/app/assets/stylesheets/partials/timer.css.scss b/app/assets/stylesheets/partials/timer.css.scss index 5bb9017..71749a9 100644 --- a/app/assets/stylesheets/partials/timer.css.scss +++ b/app/assets/stylesheets/partials/timer.css.scss @@ -42,10 +42,6 @@ * of the countdown that you don't need. */ -.count-days { display:none !important; } - -.count-div-0 { display:none !important; } - .count-div { display:inline-block; width: 10px; @@ -69,4 +65,4 @@ .count-div:after { top:0.9em; } -#timer-button { margin: 0; } \ No newline at end of file +.timer-button { margin: 0; } \ No newline at end of file diff --git a/app/controllers/time_entries_controller.rb b/app/controllers/time_entries_controller.rb index 5d5dc1c..362c986 100644 --- a/app/controllers/time_entries_controller.rb +++ b/app/controllers/time_entries_controller.rb @@ -13,35 +13,39 @@ def edit # POST /time_entries def create @time_entry = TimeEntry.new(time_entry_params.merge(user: current_user)) - - if @time_entry.save - redirect_to({ - controller: :timesheet, - action: :show, - year: @time_entry.started_at.year, - month: @time_entry.started_at.month, - day: @time_entry.started_at.day - }) - - else - render action: 'new' + @time_entry.is_billable = time_entry_params[:is_billable].to_i == 1 + + respond_to do |format| + if @time_entry.save + format.json { render :json => @time_entry.json_response(current_user, time_entry_params[:entry_date]) } + else + format.json { render :json => @time_entry.errors.full_messages, status: :unprocessable_entity } + end end + end # PATCH/PUT /time_entries/:id def update puts params.inspect - if @time_entry.update(time_entry_params) - redirect_to({ controller: 'timesheet', action: 'show' }.merge(date_from_time_entry)) - else - render action: 'edit' + + respond_to do |format| + if @time_entry.update(time_entry_params) + format.json { render :json => @time_entry.json_response(current_user, time_entry_params[:entry_date]) } + else + format.json { render :json => @time_entry.errors.full_messages, status: :unprocessable_entity } + end end end # DELETE /time_entries/:id def destroy + time_entry = { id: @time_entry.id } @time_entry.destroy - redirect_to({ controller: 'timesheet', action: 'show' }.merge(date_from_time_entry)) + respond_to do |format| + format.json { render :json => time_entry } + end + end def finish @@ -49,7 +53,9 @@ def finish flash[:notice] = flash_message(:timer_not_stopped) end - redirect_to controller: :timesheet, action: :show + respond_to do |format| + format.json { render :json => { ended_at: @time_entry.ended_at.strftime('%H:%M') } } + end end private diff --git a/app/helpers/time_entries_helper.rb b/app/helpers/time_entries_helper.rb deleted file mode 100644 index 9e3e384..0000000 --- a/app/helpers/time_entries_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -module TimeEntriesHelper - - # - # `arg` should be one of :day, :month or :year - # - def get_date_component(time_entry, arg) - arg = arg.to_sym - time_entry.persisted? ? time_entry.started_at.send(arg) : params[arg] - end - -end \ No newline at end of file diff --git a/app/helpers/timesheet_helper.rb b/app/helpers/timesheet_helper.rb index 11d72d7..722a2bc 100644 --- a/app/helpers/timesheet_helper.rb +++ b/app/helpers/timesheet_helper.rb @@ -1,29 +1,22 @@ module TimesheetHelper - def start_button(time_entry, css_class = '') - css_class = "btn btn-inverse btn-small #{css_class}".strip - hsh = { - time_entry: { - project_id: time_entry.project_id, - task_id: time_entry.task_id, - description: time_entry.description, - is_billable: time_entry.is_billable? - } - } - link_to 'Start', time_entries_path(today_date.merge(hsh)), method: :post, class: css_class - end - def stop_button(time_entry) css_class = 'btn btn-warning btn-small stop-button' - link_to 'Stop', time_entry_finish_path(time_entry.id, { time_entry: { ended_at: '' } }), - method: :patch, class: css_class + if time_entry.persisted? + link_to 'Stop', time_entry_finish_path(time_entry.id), + method: :patch, remote: true, class: css_class + else + link_to 'Stop', '#', method: :patch, remote: true, class: css_class + end end - def timer_data_id(time_entry) + def timer_duration(time_entry) if time_entry.ended_at.present? time_entry.ended_at - time_entry.started_at - else + elsif time_entry.started_at.present? Time.zone.now - time_entry.started_at + else + 0 end.ceil end @@ -36,4 +29,8 @@ def hours_and_minutes(seconds) end end + def format_datetime(datetime) + datetime.strftime("%H:%M") + end + end \ No newline at end of file diff --git a/app/models/time_entry.rb b/app/models/time_entry.rb index 05619e6..1c9ef92 100644 --- a/app/models/time_entry.rb +++ b/app/models/time_entry.rb @@ -26,6 +26,22 @@ def stop update_attribute(:ended_at, Time.zone.now) end + def json_response(user, date) + position = Timesheet.new(user).day_entries(date.to_date).map(&:id).index(id) + duration = ended_at.blank? ? Time.zone.now - started_at : ended_at - started_at + { + id: id, + project: self.project, + task: self.task, + description: description, + start_time: started_at.strftime('%H:%M'), + end_time: ended_at.present? ? ended_at.strftime('%H:%M') : nil, + duration: duration, + is_billable: is_billable, + position: position + } + end + private def set_time_parameters diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 37884bd..c988602 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -15,6 +15,12 @@ +
+
+
+
+
+
<%= render 'layouts/header' %> diff --git a/app/views/time_entries/_form.html.erb b/app/views/time_entries/_form.html.erb deleted file mode 100644 index a53b5e0..0000000 --- a/app/views/time_entries/_form.html.erb +++ /dev/null @@ -1,41 +0,0 @@ -<%= simple_form_for(@time_entry, html: { class: 'form-horizontal' }) do |f| %> - - <%= f.error_notification if f.error_notification %> - - <%= f.association :project, autofocus: true %> - - <%= f.association :task %> - - <%= f.input :is_billable, - label: 'Billable hour?', - as: :boolean %> - - <%= f.input :start_time, - as: :string, - input_html: { value: @time_entry.started_at && @time_entry.started_at.strftime('%H:%M'), - class: 'time' } %> - - <%= f.input :end_time, - as: :string, - input_html: { value: @time_entry.ended_at && @time_entry.ended_at.strftime('%H:%M'), - class: 'time' } %> - - <%= f.input :year, - as: :hidden, - input_html: { value: get_date_component(@time_entry, :year), name: 'year' } %> - - <%= f.input :month, - as: :hidden, - input_html: { value: get_date_component(@time_entry, :month), name: 'month' } %> - - <%= f.input :day, - as: :hidden, - input_html: { value: get_date_component(@time_entry, :day), name: 'day' } %> - - <%= f.input :description, as: :text, :input_html => { :class => 'time_entry_form' }%> - -
- <%= f.button :submit %> -
- -<% end %> \ No newline at end of file diff --git a/app/views/time_entries/edit.html.erb b/app/views/time_entries/edit.html.erb deleted file mode 100644 index 7981fcb..0000000 --- a/app/views/time_entries/edit.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -

Editing time entry

- -<%= render 'form' %> \ No newline at end of file diff --git a/app/views/time_entries/new.html.erb b/app/views/time_entries/new.html.erb deleted file mode 100644 index c2987f7..0000000 --- a/app/views/time_entries/new.html.erb +++ /dev/null @@ -1,5 +0,0 @@ - - -<%= render 'form' %> \ No newline at end of file diff --git a/app/views/timesheet/_time_entry_form_modal.html.erb b/app/views/timesheet/_time_entry_form_modal.html.erb new file mode 100644 index 0000000..cb037f5 --- /dev/null +++ b/app/views/timesheet/_time_entry_form_modal.html.erb @@ -0,0 +1,82 @@ + diff --git a/app/views/timesheet/_time_entry_row.html.erb b/app/views/timesheet/_time_entry_row.html.erb new file mode 100644 index 0000000..ba9a3be --- /dev/null +++ b/app/views/timesheet/_time_entry_row.html.erb @@ -0,0 +1,98 @@ + + data-entry-id="<%= time_entry.id %>" + data-project-id="<%= time_entry.project_id %>" + data-task-id="<%= time_entry.task_id %>" + data-description="<%= time_entry.description %>" + data-is-billable="<%= time_entry.is_billable %>" + data-start-time="<%= time_entry.started_at.blank? ? nil : format_datetime(time_entry.started_at) %>" + data-end-time="<%= time_entry.ended_at.blank? ? nil : format_datetime(time_entry.ended_at) %>" + > + + + + + + + + +
+
+
+ <%= format_datetime(time_entry.started_at) if time_entry.started_at.present? %> +
+
+
+
+ <%= format_datetime(time_entry.ended_at) if time_entry.ended_at.present? %> +
+
+
+ + + + + + +
+ + + + + 0 + + + 0 + + + + + + + 0 + + + 0 + + + +
+ + +
+
+ Start +
+
+ <%= stop_button(time_entry) %> +
+
+ + + + + + + <%= link_to ''.html_safe, time_entry.persisted? ? time_entry : '#', + class: 'delete-time-entry', method: :delete, remote: true, data: { confirm: 'Are you sure?' } %> + + diff --git a/app/views/timesheet/show.html.erb b/app/views/timesheet/show.html.erb index de1933b..cbcf914 100644 --- a/app/views/timesheet/show.html.erb +++ b/app/views/timesheet/show.html.erb @@ -1,3 +1,5 @@ +<%= render :partial => "time_entry_form_modal" %> +
@@ -39,10 +41,9 @@
- <%= link_to ' - Entry'.html_safe, - new_time_entry_path(working_date(params)), - class: 'btn btn-inverse btn-large entry-position' %> + + Entry +
@@ -66,123 +67,26 @@
-<% if @day_entries.empty? %> -
<%= t :no_time_entry %>
-<% else %> - - - - - - - - - - - - - <% @day_entries.each do |time_entry| %> - - - - - - - - - - <% end %> - - +
+ <%= t :no_time_entry %> +
-
ActivityTimeDurationActions
- - - - -
-
-
- <%= time_entry.started_at.strftime("%H:%M") %> -
-
-
-
- <%= time_entry.ended_at.strftime("%H:%M") unless time_entry.ended_at.nil? %> -
-
-
- -
- - -
- - - <% ['days','hours','minutes'].each_with_index do |elem, i| %> - - - - 0 - - - 0 - - <% if elem != 'minutes' %> - - <% end %> - - - <% end %> - -
- - -
- <% if time_entry.ended_at.present? %> - <%= start_button(time_entry) %> - <% else %> - <%= stop_button(time_entry) %> - <% end %> -
- -
- -
- - - - - - - -
- -
-<% end %> \ No newline at end of file + + + + + + + + + + + + + <%= render partial: 'time_entry_row', locals: { time_entry: TimeEntry.new, is_sample_row: true } %> + <% @day_entries.each do |time_entry| %> + <%= render partial: 'time_entry_row', locals: { time_entry: time_entry, is_sample_row: false } %> + <% end %> + + +
ActivityTimeDurationActions
\ No newline at end of file