#{Time.at(logged_hours).utc.strftime("%H:%M:%S")}\n#{'*'*100}"%>
17 |
18 |
19 | <% unless logged_hours == 0%>
20 | <%= render 'show_logged_time_entries' %>
21 | <% end %>
22 |
23 |
24 |
25 | <% remaining_time=time_difference-(logged_hours*3600)%>
26 | <%= t(:label_please_log_remaining)%> | <%= Time.at(remaining_time).utc.strftime("%H:%M:%S")%> | <%= t(:label_hours_for_today)%>
27 |
28 |
29 | <% unless @assigned_issues.blank?%>
30 | <%= render 'availible_open_issues' %>
31 | <% end %>
32 |
33 | <% end %>
34 | <%end%>
35 |
--------------------------------------------------------------------------------
/app/views/user_leave_reports/_options.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_tag controller: 'user_leave_reports', action: 'report' do %>
2 |
3 |
4 | <%= t(:label_date_from) %>
5 |
6 |
7 | <%= date_field_tag "user_leave_report[date_from]",
8 | (params[:user_leave_report] && params[:user_leave_report][:date_from]) || Date.today - 1.month %>
9 |
10 |
11 |
12 | <%= t(:label_date_to) %>
13 |
14 |
15 | <%= date_field_tag "user_leave_report[date_to]",
16 | (params[:user_leave_report] && params[:user_leave_report][:date_to]) || Date.today %>
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | <%= t(:label_user) %>
26 | <%= t(:label_leave_types) %>
27 | <%= t(:label_group) %>
28 |
29 |
30 |
31 | <%= select_tag("user_leave_report[selected_users][]",
32 | user_options(params[:user_leave_report] && params[:user_leave_report][:selected_users]),
33 | multiple: true) %>
34 |
35 |
36 | <%= select_tag("user_leave_report[selected_leave_types][]",
37 | leave_options(params[:user_leave_report] && params[:user_leave_report][:selected_leave_types]),
38 | multiple: true) %>
39 |
40 |
41 |
42 | <%= select_tag("user_leave_report[selected_groups][]",
43 | group_options(params[:user_leave_report] && params[:user_leave_report][:selected_groups]),
44 | multiple: true) %>
45 |
46 |
47 |
48 | <%= submit_tag 'Apply' %>
49 | <% end %>
50 |
--------------------------------------------------------------------------------
/app/views/user_time_checks/_time_report_grid.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%=define_grid @time_report_grid, hide_submit_button: true, hide_reset_button: true, hide_csv_button: false do |e|
3 |
4 | #e.column name: t(:field_user_id), html: {style: 'text-align: center'},
5 | #:attribute => 'user_id',#model:'User',
6 | #detach_with_id: 'user_filter' do |t|
7 | #t.user.name unless t.user.nil?
8 | ##t.user.grou
9 | #end
10 | e.column name: t(:field_user_id), html: {style: 'text-align: center'},
11 | :attribute => 'user_id',
12 | detach_with_id: 'user_filter' ,
13 | custom_filter: User.active.collect{|u|[u.name, u.id]} do |t|
14 | t.user.name unless t.user.nil?
15 | end
16 |
17 | e.column name: 'Average Clock-in', html: {style: 'text-align: center'} do |t|
18 | unless t.avg_check_in_time.nil?
19 | time=Time.at(t.avg_check_in_time)
20 | t= time.in_time_zone
21 | t.to_formatted_s(:time)
22 | end
23 | end
24 |
25 | e.column name: 'Average Clock-out', html: {style: 'text-align: center'} do |t|
26 | unless t.avg_check_out_time.nil?
27 | time=Time.at(t.avg_check_out_time)
28 | t= time.in_time_zone
29 | t.to_formatted_s(:time)
30 | end
31 | end
32 |
33 | e.column name: t(:field_check_in_time), html: {style: 'text-align: center' },
34 | :attribute => 'check_in_time' ,
35 | detach_with_id: 'check_in_time_filter'
36 |
37 | e.column name: t(:field_check_out_time), html: {style: 'text-align: center'},
38 | :attribute => 'check_out_time',
39 | detach_with_id: 'check_out_time_filter'
40 |
41 |
42 |
43 |
44 |
45 | e.column name: 'Average Time Spent', html: {style: 'text-align: center'} do |t|
46 |
47 |
48 | unless t.average_time.nil?
49 |
50 | output=(t.average_time.to_i/60).to_s
51 |
52 | output+':'+(t.average_time.to_i%60).to_s
53 |
54 |
55 | end
56 |
57 |
58 | end
59 |
60 |
61 | end %>
62 |
--------------------------------------------------------------------------------
/app/views/user_time_checks/_time_report_grid_monthly.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%=define_grid @time_report_grid_monthly, hide_submit_button: true, hide_csv_button: false, hide_reset_button: true do |e|
3 |
4 | e.column name: 'Month', html: {style: 'text-align: center'} do |t|
5 | #t.week
6 | Time.at(t.check_in_time).strftime("%B")
7 | end
8 |
9 | e.column name: 'Year', html: {style: 'text-align: center'} do |t|
10 | #t.week
11 | t.check_in_time.year
12 | end
13 |
14 |
15 | e.column name: t(:field_user_id), html: {style: 'text-align: center'},
16 | :attribute => 'user_id',
17 | detach_with_id: 'user_filter' ,
18 | custom_filter: User.active.collect{|u|[u.name, u.id]} do |t|
19 | t.user.name unless t.user.nil?
20 | end
21 |
22 |
23 |
24 | e.column name: 'Average Clock-in', html: {style: 'text-align: center'} do |t|
25 | unless t.avg_check_in_time.nil?
26 | time=Time.at(t.avg_check_in_time)
27 | t= time.in_time_zone
28 | t.to_formatted_s(:time)
29 | end
30 | end
31 |
32 | e.column name: 'Average Clock-out', html: {style: 'text-align: center'} do |t|
33 | unless t.avg_check_out_time.nil?
34 | time=Time.at(t.avg_check_out_time)
35 | t= time.in_time_zone
36 | t.to_formatted_s(:time)
37 | end
38 | end
39 |
40 | #e.column name: 'Average Clock-out Secs', html: {style: 'text-align: center'} do |t|
41 | #unless t.avg_check_out_time.nil?
42 |
43 | #t.avg_check_out_time
44 |
45 | #end
46 | #end
47 |
48 | e.column name: t(:field_check_in_time), html: {style: 'text-align: center' },
49 | :attribute => 'check_in_time' ,
50 | detach_with_id: 'check_in_time_filter'
51 |
52 | e.column name: t(:field_check_out_time), html: {style: 'text-align: center'},
53 | :attribute => 'check_out_time',
54 | detach_with_id: 'check_out_time_filter'
55 |
56 |
57 |
58 |
59 | e.column name: 'average_time', html: {style: 'text-align: center'} do |t|
60 | unless t.average_time.nil?
61 |
62 | output=(t.average_time.to_i/60).to_s
63 |
64 | output+':'+(t.average_time.to_i%60).to_s
65 |
66 |
67 | end
68 | end
69 |
70 |
71 | end %>
72 |
--------------------------------------------------------------------------------
/app/views/user_time_checks/_availible_open_issues.html.erb:
--------------------------------------------------------------------------------
1 |
2 | " >
3 | <%= t(:label_issue_no)%>
4 | <%= t(:label_tracker)%>
5 | <%= t(:label_status)%>
6 | <%= t(:label_subject)%>
7 | <%= t(:label_hours_to_log)%>
8 | <%= t(:label_activity)%>
9 | <%= t(:label_comments)%>
10 |
11 |
12 | <%= form_tag({:controller => :user_time_checks, :action => :create_time_entries}) do %>
13 |
14 | <% @new_time_entries.each do |new_time_entry| %>
15 | <% if new_time_entry.errors.any? %>
16 | ">
17 |
18 |
19 |
<%= pluralize(new_time_entry.errors.count, "Error") %> <%= t(:error_post_save)%>
20 |
21 | <% new_time_entry.errors.full_messages.each do |msg| %>
22 | <%= msg %>
23 | <% end %>
24 |
25 |
27 | <%end%>
28 |
29 | " >
30 | <% assigned_issue = new_time_entry.issue %>
31 | <%#if assigned_issue.status.is_closed==false%>
32 |
33 | <%= link_to assigned_issue.id, issue_path(assigned_issue.id) %>
34 | <%= Tracker.find(assigned_issue.tracker_id).name %>
35 | <%= assigned_issue.status.name %>
36 | <%= link_to assigned_issue.subject, issue_path(assigned_issue.id) %>
37 |
38 |
39 | <%= hidden_field_tag "time_entries[][issue_id]", new_time_entry.issue_id %>
40 | <%= hidden_field_tag "time_entries[][user_id]", User.current.id %>
41 | <%= hidden_field_tag "time_entries[][spent_on]", Date.today %>
42 | <%= text_field_tag "time_entries[][hours]", new_time_entry.hours, :size => 10%>
43 | <%= select_tag "time_entries[][activity_id]", options_for_select(activity_collection_for_select_options(new_time_entry))%>
44 | <%= text_field_tag "time_entries[][comments]", new_time_entry.comments, :size => 100, :maxlength => 255 %>
45 |
46 | <%# end %>
47 | <% end %>
48 |
49 | <%= submit_tag( t(:label_button_add) ) %>
50 |
51 | <% end %>
52 |
--------------------------------------------------------------------------------
/assets/stylesheets/redmine_leaves.css:
--------------------------------------------------------------------------------
1 | #leave-menu {
2 | padding: 0px 0px 30px 0px;
3 |
4 | /*position: absolute; bottom: 0px; left:6px; margin-right: -500px;*/}
5 | #leave-menu ul {margin: 0; padding: 0;}
6 | #leave-menu li {
7 | float:left;
8 | list-style-type:none;
9 | margin: 0px 0px 0px 0px;
10 | padding: 0px 0px 0px 0px;
11 | white-space:nowrap;
12 | }
13 | #leave-menu li a {
14 | display: block;
15 | color: #555;
16 |
17 | background: #FFFFFF;
18 | text-decoration: none;
19 | font-weight: bold;
20 | margin: 0;
21 | padding: 4px 10px 4px 10px;
22 | }
23 | #leave-menu li a:hover {background:#759FCF; color:#fff;}
24 | #leave-menu li a.selected, #leave-menu li a.selected:hover {background:#fff; color:#179;}
25 |
26 |
27 |
28 | #user-time-report-menu {
29 | padding: 0px 0px 30px 0px;
30 |
31 | /*position: absolute; bottom: 0px; left:6px; margin-right: -500px;*/}
32 | #user-time-report-menu ul {margin: 0; padding: 0;}
33 | #user-time-report-menu li {
34 | float:left;
35 | list-style-type:none;
36 | margin: 0px 0px 0px 0px;
37 | padding: 0px 0px 0px 0px;
38 | white-space:nowrap;
39 | }
40 | #user-time-report-menu li a {
41 | display: block;
42 | color: #555;
43 |
44 | background: #FFFFFF;
45 | text-decoration: none;
46 | font-weight: bold;
47 | margin: 0;
48 | padding: 4px 10px 4px 10px;
49 | }
50 | #user-time-report-menu li a:hover {background:#759FCF; color:#fff;}
51 | #user-time-report-menu li a.selected, #user-time-report-menu li a.selected:hover {background:#fff; color:#179;}
52 |
53 |
54 |
55 |
56 |
57 |
58 | #user-time-analytics-menu {
59 | padding: 0px 0px 30px 0px;
60 |
61 | /*position: absolute; bottom: 0px; left:6px; margin-right: -500px;*/}
62 | #user-time-analytics-menu ul {margin: 0; padding: 0;}
63 | #user-time-analytics-menu li {
64 | float:left;
65 | list-style-type:none;
66 | margin: 0px 0px 0px 0px;
67 | padding: 0px 0px 0px 0px;
68 | white-space:nowrap;
69 | }
70 | #user-time-analytics-menu li a {
71 | display: block;
72 | color: #555;
73 |
74 | background: #FFFFFF;
75 | text-decoration: none;
76 | font-weight: bold;
77 | margin: 0;
78 | padding: 4px 10px 4px 10px;
79 | }
80 | #user-time-analytics-menu li a:hover {background:#759FCF; color:#fff;}
81 | #user-time-analytics-menu li a.selected, #user-time-analytics-menu li a.selected:hover {background:#fff; color:#179;}
82 |
83 |
--------------------------------------------------------------------------------
/app/views/user_time_checks/_time_report_grid_weekly.erb:
--------------------------------------------------------------------------------
1 |
2 | <%=define_grid @time_report_grid_weekly, hide_submit_button: true, hide_csv_button: false, hide_reset_button: true do |e|
3 |
4 |
5 | e.column name: 'Week # ', html: {style: 'text-align: center'} do |t|
6 | t.week
7 | # time=Time.at(t.week)
8 | # time.strftime("%W")
9 | end
10 | e.column name: 'Week Start Date', html: {style: 'text-align: center'} do |t|
11 |
12 | time=Time.at(t.weekdays.beginning_of_week)
13 | time.strftime("%F")
14 | end
15 | e.column name: 'Week End Date', html: {style: 'text-align: center'} do |t|
16 | time=Time.at(t.weekdays.end_of_week)
17 | time.strftime("%F")
18 | end
19 |
20 |
21 | e.column name: 'Year', html: {style: 'text-align: center'} do |t|
22 | #t.week
23 | # Time.at(t.year).year
24 | t.year
25 | end
26 |
27 |
28 |
29 | e.column name: t(:field_user_id), html: {style: 'text-align: center'},
30 | :attribute => 'user_id',
31 | detach_with_id: 'user_filter' ,
32 | custom_filter: User.active.collect{|u|[u.name, u.id]} do |t|
33 | t.user.name unless t.user.nil?
34 | end
35 |
36 |
37 | e.column name: 'Average Clock-in', html: {style: 'text-align: center'} do |t|
38 | unless t.avg_check_in_time.nil?
39 | time=Time.at(t.avg_check_in_time)
40 | t= time.in_time_zone
41 | t.to_formatted_s(:time)
42 | end
43 | end
44 |
45 | e.column name: 'Average Clock-out', html: {style: 'text-align: center'} do |t|
46 | unless t.avg_check_out_time.nil?
47 | time=Time.at(t.avg_check_out_time)
48 | t= time.in_time_zone
49 | t.to_formatted_s(:time)
50 | end
51 | end
52 |
53 | e.column name: t(:field_check_in_time), html: {style: 'text-align: center' },
54 | :attribute => 'check_in_time' , detach_with_id: 'check_in_time_filter'
55 |
56 | e.column name: t(:field_check_out_time), html: {style: 'text-align: center'},
57 | :attribute => 'check_out_time', detach_with_id: 'check_out_time_filter'
58 |
59 |
60 |
61 | e.column name: 'Average Time Spent', html: {style: 'text-align: center'},
62 | :attribute => 'comments',#filter_type: :range,
63 | filter: false do |t|
64 | unless t.average_time.nil?
65 |
66 | output=(t.average_time.to_i/60).to_s
67 |
68 | output+':'+(t.average_time.to_i%60).to_s
69 |
70 |
71 | end
72 |
73 | end
74 |
75 |
76 | end %>
77 |
--------------------------------------------------------------------------------
/app/views/user_leaves/new.html.erb:
--------------------------------------------------------------------------------
1 | <%= render template: "layouts/redmine_leaves" %>
2 | <%= t(:label_add_leave ) %>
3 | <%= form_for UserLeave.new do %>
4 |
5 |
6 | <%= t(:label_user_group) %>
7 | <%= select_tag("create_user_leave[selected_users][]",
8 | add_user_options(params[:create_user_leave] && params[:create_user_leave][:selected_users]),
9 | multiple: true) %>
10 | <%= select_tag('create_user_leave[selected_groups][]',
11 | add_group_options(params[:create_user_leave] && params[:create_user_leave][:selected_groups]),
12 | multiple: true, disabled: @disable_group_leave) %>
13 |
14 |
15 |
16 | <%= t(:label_leave_type ) %>
17 | <%= select_tag('create_user_leave[selected_leave]',
18 | add_leave_options(params[:create_user_leave] && params[:create_user_leave][:selected_leave])) %>
19 |
20 |
21 |
22 |
23 |
24 | <%= t(:label_leave_date ) %>
25 | <%= t(:label_date_from) %>
26 | <%= text_field_tag("create_user_leave[selected_date_from]",
27 | (params[:create_user_leave] && params[:create_user_leave][:selected_date_from]) || Date.today) %>
28 | <%= calendar_for 'create_user_leave_selected_date_from' %>
29 | <%= t(:label_date_to) %>
30 | <%= text_field_tag("create_user_leave[selected_date_to]",
31 | (params[:create_user_leave] && params[:create_user_leave][:selected_date_to]) || Date.today) %>
32 | <%= calendar_for 'create_user_leave_selected_date_to' %>
33 |
34 |
35 |
36 |
37 |
38 | <%= t(:label_comments) %>
39 |
40 | <%= text_field_tag "create_user_leave[comments]",
41 | params[:create_user_leave] && params[:create_user_leave][:comments], size: 28 %>
42 |
43 | <%= t(:label_optional ) %>
44 |
45 |
46 |
47 | <%= t(:label_fractional_leave) %>
48 |
49 | <%= number_field_tag "create_user_leave[fractional_leave]",
50 | params[:create_user_leave] && params[:create_user_leave][:fractional_leave],
51 | :step =>'.25', min: -2,max: 2.5 %>
52 | <%= t(:label_fractional_leave_optional ) %>
53 |
54 |
55 | <%= submit_tag t(:label_button_add) %>
56 |
57 | <% end %>
58 |
59 |
--------------------------------------------------------------------------------
/app/views/common/_calendar.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% 7.times do |i| %><%= day_name( (calendar.first_wday+i)%7 ) %> <% end %>
4 |
5 |
6 |
7 | <% day = calendar.startdt
8 | while day <= calendar.enddt %>
9 | <%= ("#{(day+(11-day.cwday)%7).cweek} ".html_safe) if day.cwday == calendar.first_wday %>
10 |
11 | <%= day.day %>
12 | <% calendar.events_with_leaves_on(day,@project).each do |i| %>
13 | <% if i.is_a? Issue %>
14 |
15 | <%= h("#{i.project} -") unless @project && @project == i.project %>
16 | <%= link_to_issue i, :truncate => 30 %>
17 | <%= render_issue_tooltip i %>
18 |
19 | <% elsif i.is_a? Version %>
20 |
21 | <%= h("#{i.project} -") unless @project && @project == i.project %>
22 | <%= link_to_version i%>
23 |
24 | <% elsif i.is_a? UserLeave %>
25 |
26 |
27 | <%= link_to "#{User.find(i.user_id).name}", user_url(User.find(i.user_id).id) %>
28 | <%= "- #{i.fractional_leave}" %>
29 |
30 | <%=t(:label_name)%>: <%=link_to "#{User.find(i.user_id).name}", user_url(User.find(i.user_id).id) %>
31 | <%=t(:label_leave_type)%>: <%= i.leave_type %>
32 | <%=t(:label_leave_date)%>: <%= i.leave_date %>
33 | <%=t(:label_comments)%>: <%= i.comments %>
34 | <%=t(:label_weight)%>: <%= i.fractional_leave %>
35 |
36 |
37 | <% end %>
38 |
39 | <% end %>
40 |
41 | <%= ' '.html_safe if day.cwday==calendar.last_wday and day!=calendar.enddt %>
42 | <% day = day + 1
43 | end %>
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/controllers/user_leave_reports_controller.rb:
--------------------------------------------------------------------------------
1 | class UserLeaveReportsController < ApplicationController
2 | unloadable
3 |
4 | before_action :require_login
5 |
6 | include UserLeaveReportsHelper
7 |
8 | def index
9 | @all_users = User.all
10 | @all_leaves = plugin_setting('leave_types')
11 | end
12 |
13 | def report
14 | if params[:user_leave_report].nil?
15 | flash.now[:error] = t(:error_no_params )
16 | return
17 | end
18 |
19 | user_leaves = nil
20 | where_statements = []
21 | where_clause = ['']
22 | selected_groups = params[:user_leave_report][:selected_groups]
23 |
24 | unless (params[:user_leave_report][:selected_users].nil? && selected_groups.nil?)
25 | where_statements << 'user_id IN (?)'
26 | all_users=[]
27 | all_users << params[:user_leave_report][:selected_users]
28 |
29 | unless selected_groups.nil?
30 | selected_groups.each do |selected_group|
31 | group=Group.where(lastname: selected_group)
32 | all_users << group.first.users unless group.first.users.nil?
33 | end
34 | end
35 | where_clause << all_users.flatten.uniq
36 | end
37 |
38 | selected_leave_types = params[:user_leave_report][:selected_leave_types]
39 | unless selected_leave_types.nil?
40 | where_statements << 'leave_type IN (?)'
41 | where_clause << selected_leave_types
42 | end
43 |
44 | unless params[:user_leave_report][:date_from].blank?
45 | begin
46 | where_statements << 'leave_date >= ?'
47 | where_clause << params[:user_leave_report][:date_from].to_date
48 | rescue StandardError
49 | flash.now[:error] = t(:error_invalid_date)
50 | return
51 | end
52 | end
53 |
54 | unless params[:user_leave_report][:date_to].blank?
55 | begin
56 | where_statements << 'leave_date <= ?'
57 | where_clause << params[:user_leave_report][:date_to].to_date
58 | rescue StandardError
59 | flash.now[:error] = t(:error_invalid_date)
60 | return
61 | end
62 | end
63 |
64 | where_clause[0] = where_statements.join(' AND ')
65 | @user_leave = UserLeave.where(where_clause).order('leave_date desc')
66 |
67 | if @user_leave .nil? || @user_leave .empty?
68 | flash.now[:error] = t(:error_no_results)
69 | end
70 |
71 | @leaves_report_grid = initialize_grid(@user_leave.order(id: :desc),
72 | name: 'grid',
73 | enable_export_to_csv: true,
74 | csv_field_separator: ';',
75 | csv_file_name: 'LeavesReport')
76 |
77 | export_grid_if_requested('grid' => 'grid')
78 |
79 |
80 |
81 |
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/app/models/leave_mailer.rb:
--------------------------------------------------------------------------------
1 | class LeaveMailer < ActionMailer::Base
2 | layout 'mailer'
3 | default from: Setting.mail_from
4 | helper ApplicationHelper
5 | def self.default_url_options
6 | ::Mailer.default_url_options
7 | end
8 |
9 | def cc_role_email_addresses(user)
10 | cc_role_ids = Setting.plugin_redmine_leaves['cc_roles']
11 | if cc_role_ids.present?
12 | User.active.preload(:email_address).joins(members: :member_roles).where("#{Member.table_name}.project_id" => user.projects.pluck(:id)).
13 | where("#{MemberRole.table_name}.role_id IN (?)", cc_role_ids).map(&:email_address).map(&:address)
14 | else
15 | []
16 | end
17 | end
18 | private :cc_role_email_addresses
19 |
20 | def cc_email_addresses(user)
21 | emails = User.in_group(UserTimeCheck.time_log_receivers_group).map(&:mail)
22 | emails += cc_role_email_addresses(user)
23 | emails.uniq
24 | end
25 | private :cc_email_addresses
26 |
27 | def notify_absentee(user_leave)
28 | @total_yearly_leaves = UserLeave.where(user_id: user_leave.user_id, leave_type: user_leave.leave_type).where("leave_date >= ?", Date.today.beginning_of_year).sum(:fractional_leave)
29 | @leave = user_leave
30 | mail(to: @leave.user.mail,
31 | cc: cc_email_addresses(@leave.user),
32 | subject: I18n.t('subject_leave_marked_for',
33 | user_name: @leave.user.name,
34 | fraction: @leave.fractional_leave, total_yearly_leaves: @total_yearly_leaves,
35 | leave_type: @leave.leave_type, leave_date: I18n.l(@leave.leave_date)))
36 | end
37 |
38 | #send project timesheet
39 | def project_timesheet(users, timesheet_table, project, start_date, end_date)
40 | @timesheet_table = timesheet_table
41 | @report_date = start_date == end_date ? I18n.l(end_date) : "#{I18n.l(start_date)} - #{I18n.l(end_date)}"
42 | mail(to: users.map(&:mail), subject: I18n.t('subject_project_timesheet',
43 | project: project, report_date: @report_date))
44 | end
45 |
46 | def group_timesheet(users, timesheet_table, group, start_date, end_date)
47 | @timesheet_table = timesheet_table
48 | @report_date = start_date == end_date ? I18n.l(end_date) : "#{I18n.l(start_date)} - #{I18n.l(end_date)}"
49 | mail(to: users.map(&:mail), subject: I18n.t('subject_group_timesheet',
50 | group: group.lastname, report_date: @report_date))
51 | end
52 |
53 | def missing_time_log(user, start_date, end_date, logged_hours)
54 | @user = user
55 | @report_date = start_date == end_date ? I18n.l(end_date) : "#{I18n.l(start_date)} - #{I18n.l(end_date)}"
56 | @logged_hours = logged_hours
57 | mail(to: user.mail,
58 | cc: cc_email_addresses(user),
59 | subject: I18n.t('subject_missing_time_log',
60 | user_name: user.name, report_date: @report_date))
61 | end
62 |
63 | end
--------------------------------------------------------------------------------
/app/views/user_leave_analytics/report.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :header_tags do %>
2 | <%= javascript_include_tag "highcharts", plugin: 'redmine_leaves' %>
3 | <% end %>
4 |
5 | <%= form_tag controller: 'user_leave_analytics', action: 'report' do %>
6 |
7 | <%= render template: "layouts/redmine_leaves" %>
8 | <%= t(:title_leave_analytics) %>
9 |
10 |
11 |
12 | <%= t(:label_date_from) %>
13 | <%= text_field_tag "user_leave_analytic[date_from]",
14 | (params[:user_leave_analytic] && params[:user_leave_analytic][:date_from]) || (Date.today.beginning_of_year) %>
15 | <%= calendar_for 'user_leave_analytic_date_from' %>
16 |
17 | <%= t(:label_date_to) %>
18 | <%= text_field_tag "user_leave_analytic[date_to]",
19 | (params[:user_leave_analytic] && params[:user_leave_analytic][:date_to]) || Date.today %>
20 | <%= calendar_for 'user_leave_analytic_date_to' %>
21 |
22 | <%= submit_tag 'Apply' %>
23 |
24 | <%= t(:label_user) %>
25 | <%= select_tag "user_leave_analytic[selected_user]",
26 | user_options(@user.id),
27 | {onchange: "this.form.submit();"} %>
28 |
29 |
30 |
31 |
32 | <%= high_chart("my_b1", @bar1) %>
33 |
34 |
35 |
36 | <%= high_chart("my_p1", @pie1) do |c| %>
37 | <%= raw "options.tooltip.formatter = function() {return this.point.name +': '+ this.y +' %';}" %>
38 | <%= raw "options.plotOptions.pie.dataLabels.formatter = function() { if (this.y > 5) return this.point.name; }" %>
39 | <% end %>
40 |
41 |
42 |
43 | <%= high_chart("my_b2", @bar2) %>
44 |
45 |
46 |
47 | <%= high_chart("my_p2", @pie2) do |c| %>
48 | <%= raw "options.tooltip.formatter = function() {return this.point.name +': '+ this.y +' %';}" %>
49 | <%= raw "options.plotOptions.pie.dataLabels.formatter = function() { if (this.y > 5) return this.point.name; }" %>
50 | <% end %>
51 |
52 |
53 |
54 | <%= high_chart("my_b3", @bar3) %>
55 |
56 | <% end %>
57 |
58 |
59 | <%= high_chart("my_p3", @pie3) do |c| %>
60 | <%= raw "options.tooltip.formatter = function() {return this.point.name +': '+ this.y +' %';}" %>
61 | <%= raw "options.plotOptions.pie.dataLabels.formatter = function() { if (this.y > 5) return this.point.name; }" %>
62 | <% end %>
63 |
64 |
--------------------------------------------------------------------------------
/app/helpers/user_leaves_helper.rb:
--------------------------------------------------------------------------------
1 | module UserLeavesHelper
2 |
3 | def plugin_setting(setting_name)
4 | (Setting.plugin_redmine_leaves[setting_name] || '').split(',').delete_if { |index| index.blank? }
5 | end
6 |
7 | def mark_leave_options(mark_leave)
8 | User.active.joins(:groups).
9 | where("#{User.table_name_prefix}groups_users#{User.table_name_suffix}.id" => mark_leave)
10 | end
11 |
12 | def add_user_options(selected_user)
13 | mark_leave_groups = Setting.plugin_redmine_leaves['mark_leaves']
14 | mark_leave_users = mark_leave_options(mark_leave_groups)
15 |
16 | mark_own_leave_groups = Setting.plugin_redmine_leaves['mark_own_leave']
17 | mark_own_leave_users = mark_leave_options(mark_own_leave_groups)
18 |
19 | time_loggers_group = Group.find(Setting.plugin_redmine_leaves['time_loggers_group'])
20 | if(mark_leave_users.include?(User.current) && mark_own_leave_users.include?(User.current))
21 | # if current user have both permissions
22 | all_users = User.in_group(time_loggers_group)
23 | elsif(mark_leave_users.include?(User.current) && !mark_own_leave_users.include?(User.current))
24 | # if current user have permission to mark leaves only
25 | all_users = User.in_group(time_loggers_group).where.not(id: User.current.id)
26 | elsif(!mark_leave_users.include?(User.current) && mark_own_leave_users.include?(User.current))
27 | # if current user have permission to mark own leaves
28 | all_users = User.where(id: User.current.id).active
29 | else
30 | all_users = User.active
31 | end
32 |
33 | options_from_collection_for_select(all_users.sort_by{|e| e[:firstname]}, :id, :name, selected_user)
34 |
35 | end
36 |
37 | def add_group_options(selected_group)
38 | mark_leave_groups = Setting.plugin_redmine_leaves['mark_leaves']
39 | mark_leave_users = mark_leave_options(mark_leave_groups)
40 |
41 | mark_own_leave_groups = Setting.plugin_redmine_leaves['mark_own_leave']
42 | mark_own_leave_users = mark_leave_options(mark_own_leave_groups)
43 |
44 | unless(!mark_leave_users.include?(User.current) && mark_own_leave_users.include?(User.current))
45 | # if current user have permission to mark own leaves
46 | @disable_group_leave = false
47 | options_from_collection_for_select(Group.all, :id, :name, selected_group)
48 | else
49 | @disable_group_leave = true
50 | options_from_collection_for_select(Group.all, :id, :name, selected_group)
51 | end
52 | end
53 |
54 | def add_leave_options(selected_leave_type)
55 | all_leave_types = plugin_setting('leave_types')
56 | options_for_select(all_leave_types, selected_leave_type || Setting.plugin_redmine_leaves['default_type'])
57 | end
58 |
59 | def check_selected_users(selected_users)
60 | mark_leave_groups = Setting.plugin_redmine_leaves['mark_leaves']
61 | mark_leave_users = mark_leave_options(mark_leave_groups)
62 |
63 | mark_own_leave_groups = Setting.plugin_redmine_leaves['mark_own_leave']
64 | mark_own_leave_users = mark_leave_options(mark_own_leave_groups)
65 | if(mark_leave_users.include?(User.current) && !mark_own_leave_users.include?(User.current))
66 | selected_users = selected_users - [User.current.id.to_s]
67 | end
68 | selected_users
69 | end
70 |
71 | end
72 |
--------------------------------------------------------------------------------
/init.rb:
--------------------------------------------------------------------------------
1 | require 'redmine'
2 |
3 | Rails.configuration.to_prepare do
4 | require_dependency 'user'
5 | User.send(:include, RedmineLeaves::Patches::UserPatch)
6 |
7 | require_dependency 'redmine/helpers/calendar'
8 | Redmine::Helpers::Calendar.send(:include, RedmineLeaves::Patches::LeavesInCalendarPatch)
9 |
10 | require_dependency 'time_entry'
11 | TimeEntry.send(:include, RedmineLeaves::Patches::TimeRestrictionPatch)
12 | end
13 |
14 | require 'wice_grid_config'
15 |
16 | Redmine::Plugin.register :redmine_leaves do
17 | name 'Redmine Leaves Plugin'
18 | author 'Arkhitech'
19 | description 'This is a plugin for user check-in/check-out'
20 | url 'http://github.com/arkhitech/redmine_leaves'
21 | author_url 'https://github.com/arkhitech'
22 | version '0.1.0'
23 |
24 | permission :receive_timesheet_email, { }, require: :member
25 |
26 | permission :view_time_reports, user_time_check: :user_time_reporting
27 |
28 |
29 | menu :top_menu, :time_check_in, { controller: 'user_time_checks', action: 'check_in' },
30 | caption: :caption_top_menu_check_in, if: Proc.new {!UserTimeCheck.checked_in?(User.current.id)}, last: true
31 | menu :top_menu, :time_check_out, { controller: 'user_time_checks', action: 'check_out' },
32 | caption: :caption_top_menu_check_out, if: Proc.new {UserTimeCheck.checked_in?(User.current.id)}, last: true
33 |
34 | menu :top_menu, :user_leave_reports, { controller: 'user_leave_reports', action: 'index' }, caption: :caption_leave_report
35 |
36 | menu :top_menu, :user_time_checks, { controller: 'user_time_checks', action: 'index' }, caption: :caption_user_time_check
37 |
38 | menu :top_menu, :user_time_reporting, { controller: 'user_time_checks', action: 'user_time_reporting' }, :caption => 'User Time Reports'
39 | menu :user_time_report_menu, :user_time_report_custom, { :controller => 'user_time_checks', :action => 'user_time_reporting'}, :caption => 'Custom'
40 | menu :user_time_report_menu, :user_time_report_weekly, { :controller => 'user_time_checks', :action => 'user_time_reporting_weekly'}, :caption => 'Weekly'
41 | menu :user_time_report_menu, :user_time_report_monthly, { :controller => 'user_time_checks', :action => 'user_time_reporting_monthly'}, :caption => 'Monthly'
42 |
43 | menu :top_menu, :user_time_activity_report, { controller: 'user_time_checks', action: 'user_time_activity_report' }, :caption => 'User Time-Activity Report'
44 | menu :user_time_analytics_menu, :user_time_activity_report_custom, { :controller => 'user_time_checks', :action => 'user_time_activity_report'}, :caption => 'All Time'
45 | menu :user_time_analytics_menu, :user_time_activity_report_monthly, { :controller => 'user_time_checks', :action => 'user_time_activity_report_monthly'}, :caption => 'Monthly'
46 | #
47 |
48 |
49 |
50 |
51 |
52 |
53 | menu :leave_report_menu, :user_leave_reports, { :controller => 'user_leave_reports', :action => 'index' }, :caption => 'Overview'
54 | menu :leave_report_menu, :user_leaves, { :controller => 'user_leaves', :action => 'new' }, :caption => 'Add Leave'
55 | menu :leave_report_menu, :user_leave_analytics, { :controller => 'user_leave_analytics', :action => 'report'}, :caption => 'Analytics'
56 |
57 | settings default: {
58 | 'leave_types' => 'Annual, Sick, Unannounced',
59 | 'default_type' => 'Annual',
60 | 'time_loggers_group' => 'Staff',
61 | 'time_log_receivers_group' => 'HR',
62 | 'num_min_working_hours' => '8',
63 | 'max_past_timelog_insert_days' => '7'
64 | }, partial: 'settings'
65 | end
66 |
--------------------------------------------------------------------------------
/app/helpers/user_leave_reports_helper.rb:
--------------------------------------------------------------------------------
1 | module UserLeaveReportsHelper
2 |
3 | def mark_leave_users
4 | @mark_leave_users ||= begin
5 | mark_leave_groups = Setting.plugin_redmine_leaves['mark_leaves']
6 | if mark_leave_groups.present?
7 | User.active.joins(:groups).
8 | where("#{User.table_name_prefix}groups_users#{User.table_name_suffix}.id" => mark_leave_groups)
9 | else
10 | User.active.all
11 | end
12 | end
13 | end
14 |
15 | def mark_own_leave_users
16 | @mark_own_leave_users ||= begin
17 | mark_own_leave_groups = Setting.plugin_redmine_leaves['mark_own_leave']
18 | User.active.joins(:groups).
19 | where("#{User.table_name_prefix}groups_users#{User.table_name_suffix}.id" => mark_own_leave_groups)
20 | end
21 | end
22 |
23 | def user_allowed_to_edit_leaves?
24 | User.current.admin? || edit_attendance_users.include?(User.current) || edit_own_attendance_users.include?(User.current)
25 | end
26 |
27 | def user_allowed_to_edit_leave?(user_leave)
28 | return true if User.current.admin?
29 | return true if user_leave.user != User.current && edit_attendance_users.include?(User.current)
30 | return true if user_leave.user == User.current && edit_own_attendance_users.include?(User.current)
31 | #else
32 | false
33 | end
34 | def edit_attendance_users
35 | @edit_attendance_users ||= begin
36 | edit_attendance_groups = Setting.plugin_redmine_leaves['edit_attendance']
37 | User.active.joins(:groups).
38 | where("#{User.table_name_prefix}groups_users#{User.table_name_suffix}.id" => edit_attendance_groups)
39 | end
40 | end
41 | def edit_own_attendance_users
42 | @edit_own_attendance_users ||= begin
43 | edit_own_attendance_groups = Setting.plugin_redmine_leaves['edit_own_attendance']
44 | User.active.joins(:groups).
45 | where("#{User.table_name_prefix}groups_users#{User.table_name_suffix}.id" => edit_own_attendance_groups)
46 | end
47 | end
48 | def plugin_setting(setting_name)
49 | (Setting.plugin_redmine_leaves[setting_name] || '').split(',').delete_if { |index| index.blank? }
50 | end
51 |
52 | def eligible_for_leave_users
53 | group_ids = Setting.plugin_redmine_leaves['eligible_for_leave_groups']
54 | if group_ids.blank?
55 | time_loggers_group = Group.find(Setting.plugin_redmine_leaves['time_loggers_group'])
56 | @eligible_users = User.in_group(time_loggers_group)
57 | # @eligible_users.order('users.name ASC').all.map{ |c| [c.name, c.id] }
58 | @eligible_users.sort_by{|e| e[:firstname]}
59 | else
60 | @eligible_users= User.active.joins(:groups).
61 | where("#{User.table_name_prefix}groups_users#{User.table_name_suffix}.id" =>
62 | group_ids).group("#{User.table_name}.id")
63 | @eligible_users.sort_by{|e| e[:firstname]}
64 |
65 | end
66 | end
67 |
68 | def user_options(selected_users)
69 | options_from_collection_for_select(eligible_for_leave_users, :id, :name, selected_users)
70 | end
71 |
72 | def leave_options(selected_leave_types)
73 | all_leave_types = plugin_setting('leave_types')
74 | options_for_select(all_leave_types, selected_leave_types)
75 | end
76 |
77 | def group_options(selected_groups)
78 | all_group_types = Group.all
79 | options_for_select(all_group_types, selected_groups)
80 | end
81 | def group_by_options(selected_group_by)
82 | group_by = [[t(:options_group_by_title ),'Leave type'],
83 | [t(:option_group_by_user),'User'],
84 | [t(:option_group_by_date),'Date']]
85 | options_for_select(group_by, selected_group_by)
86 | end
87 |
88 | end
89 |
--------------------------------------------------------------------------------
/app/views/user_leave_reports/_grid.html.erb:
--------------------------------------------------------------------------------
1 | <%leave_types_totals = {}%>
2 |
3 | <%=grid(@leaves_report_grid,hide_submit_button: true, hide_csv_button: false, hide_reset_button: true ,:show_filters => :always,html: {class: 'my-grid'}, header_tr_html: {class: 'my-header'}) do |g|
4 |
5 | g.column name: t(:label_name), in_csv: false do |user_leave|
6 | link_to user_leave.user.name, user_path(user_leave.user)
7 | end
8 | g.column name: t(:label_name), in_html: false do |user_leave|
9 | user_leave.user.name
10 | end
11 |
12 |
13 | g.column name: t(:label_leave_type), in_csv: false, attribute: 'leave_type', detach_with_id: :leave_type_filter do |user_leave|
14 | link_to user_leave.leave_type, controller: 'user_leave_reports', action: 'report', user_leave_report:{selected_leave_types: user_leave.leave_type, selected_group_by: 'User'}
15 | end
16 | g.column name: t(:label_leave_type), in_html: false, attribute: 'leave_type', detach_with_id: :leave_type_filter do |user_leave|
17 | user_leave.leave_type
18 | end
19 |
20 | g.column name: t(:label_leave_date), in_csv: false, attribute: 'leave_date', detach_with_id: :leave_date_filter do |user_leave|
21 | link_to format_date(user_leave.leave_date), controller: 'user_leave_reports', action: 'report', user_leave_report:{date_from: user_leave.leave_date,date_to: user_leave.leave_date, selected_group_by: 'User'}
22 | end
23 | g.column name: t(:label_leave_date), in_html: false, attribute: 'leave_date', detach_with_id: :leave_date_filter do |user_leave|
24 | format_date user_leave.leave_date
25 | end
26 |
27 | g.column name: t(:label_weight), attribute: 'fractional_leave', detach_with_id: :fractional_leave_filter do |user_leave|
28 |
29 | leave_types_totals [user_leave.leave_type] ||= 0
30 | leave_types_totals [user_leave.leave_type] += user_leave.fractional_leave
31 |
32 | user_leave.fractional_leave
33 |
34 | end
35 |
36 |
37 | g.column name: t(:label_comments), attribute: 'comments', detach_with_id: :comments_filter do |user_leave|
38 | user_leave.comments
39 | end
40 |
41 | g.column name: t(:field_updated_on), in_csv: false, attribute: 'updated_at', detach_with_id: 'updated_filter' do |user_leave|
42 | format_date(user_leave.updated_at)
43 | end
44 | g.column name: t(:field_updated_on), in_html: false, attribute: 'updated_at', detach_with_id: 'updated_filter' do |user_leave|
45 | format_date(user_leave.updated_at)
46 | end
47 |
48 | g.column name: t(:field_created_on), in_csv: false, attribute: 'created_at', detach_with_id: 'created_filter' do |user_leave|
49 | format_date(user_leave.created_at)
50 | end
51 | g.column name: t(:field_created_on), in_html: false, attribute: 'created_at', detach_with_id: 'created_filter' do |user_leave|
52 | format_date(user_leave.created_at)
53 | end
54 |
55 | if user_allowed_to_edit_leaves?
56 | g.column name: 'Edit', in_csv: false do |user_leave|
57 | link_to t(:link_edit), edit_user_leafe_path(user_leave)
58 | end
59 | g.column name: 'Delete', in_csv: false do |user_leave|
60 | link_to t(:link_delete), user_leafe_path(user_leave), :remote => true, method: :delete, data: { confirm: t(:label_confirm) }
61 | end
62 | end
63 |
64 |
65 | g.last_row do |number_of_columns|
66 |
67 | leave_types_string = []
68 | leave_weights_string=[]
69 |
70 | leave_types_totals.each_pair do |key, value|
71 | leave_types_string << key
72 | leave_weights_string << '%.2f' % leave_types_totals[key]
73 | end
74 |
75 |
76 | "
77 | Total
78 | #{leave_types_string.join(' ')}
79 |
80 | #{leave_weights_string.join(' ')}
81 |
82 |
83 | "
84 |
85 | end
86 |
87 |
88 | end -%>
89 |
90 |
91 |
--------------------------------------------------------------------------------
/app/controllers/user_leaves_controller.rb:
--------------------------------------------------------------------------------
1 | class UserLeavesController < ApplicationController
2 | unloadable
3 |
4 | include UserLeaveReportsHelper
5 | include UserLeavesHelper
6 |
7 | def new
8 | if !(!mark_leave_users.include?(User.current) && !mark_own_leave_users.include?(User.current))
9 | @user_leave = UserLeave.new
10 | else
11 | return deny_access
12 | end
13 | end
14 |
15 | def create
16 | errors = []
17 | selected_users = []
18 | if params['create_user_leave']['selected_users']
19 | selected_users = params['create_user_leave']['selected_users']
20 | else
21 | selected_users = User.active.joins(:groups).
22 | where("#{User.table_name_prefix}groups_users#{User.table_name_suffix}.id" => params['create_user_leave']['selected_groups']).map(&:id)
23 | end
24 | if params['create_user_leave']['selected_date_from'].blank? ||
25 | params['create_user_leave']['selected_date_to'].blank?
26 | errors << "Date Field(s) cannot be empty!"
27 | else
28 | begin
29 | selected_date_from = params['create_user_leave']['selected_date_from'].to_date#.map{|k,v| v}.join("-").to_date
30 | selected_date_to = params['create_user_leave']['selected_date_to'].to_date#.map{|k,v| v}.join("-").to_date
31 | rescue
32 | errors << "Invalid Date Format!"
33 | end
34 | end
35 | selected_users = check_selected_users(selected_users)
36 | notices = []
37 | if !selected_users.empty? && errors.empty?
38 | selected_users = selected_users.uniq
39 | selected_users.each do |user|
40 | leave_date = selected_date_from
41 | while leave_date <= selected_date_to
42 | user_leave = UserLeave.new(user_id: user.to_i, leave_type: params['create_user_leave']['selected_leave'],
43 | leave_date: leave_date, comments: params['create_user_leave']['comments'],
44 | fractional_leave: params['create_user_leave']['fractional_leave'])
45 | leave_date += 1
46 | unless user_leave.save
47 | errors << t(:error_leave_add, user_name: user_leave.user.name, leave_type: user_leave.leave_type,
48 | leave_date: user_leave.leave_date, reason: user_leave.errors.full_messages.join(' '))
49 | else
50 | total_yearly_leaves = UserLeave.where(user_id: user, leave_type: user_leave.leave_type).where("leave_date >= ?", Date.today.beginning_of_year).sum(:fractional_leave)
51 | notices << t(:notice_leave_add, user_name: user_leave.user.name,
52 | leave_type: user_leave.leave_type,
53 | leave_date: user_leave.leave_date, total_yearly_leaves: total_yearly_leaves)
54 | end
55 | end
56 | end
57 | errors = errors.flatten.uniq
58 | notices = notices.flatten.uniq
59 | unless errors.blank?
60 | flash.now[:notice] = "#{notices.join(' ')}"
61 | flash.now[:error] = "#{errors.join(' ')}"
62 | render 'new'
63 | else
64 | redirect_to user_leave_reports_path, notice: "#{notices.join(' ')}"
65 | end
66 | else
67 | flash.now[:error] = t(:error_no_user_group_selected)
68 | render 'new'
69 | end
70 | end
71 |
72 | def edit
73 | @user_leave = UserLeave.find(params[:id])
74 | end
75 |
76 | def update
77 | @user_leave = UserLeave.find(params[:id])
78 | if @user_leave.update_attributes(params.require(:user_leave).permit!)
79 | redirect_to edit_user_leafe_path(@user_leave), notice: t(:notice_leaves_updated)
80 | else
81 | redirect_to edit_user_leafe_path(@user_leave), error: t(:error_leaves_not_updated)
82 | end
83 | end
84 |
85 | def destroy
86 | @user_leave = UserLeave.find(params[:id])
87 | @user_leave.destroy
88 | respond_to do |format|
89 | format.js {}
90 | end
91 | end
92 | end
93 |
94 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Redmine Leaves
2 | ==============
3 |
4 | A simple plugin for user leaves management. It allows user to check-in and check-out for attendance marking.
5 | Leave is marked for any user that does not check-in by the specified time.
6 | Administrator can define leave types via the administrative interface for the plugin.
7 |
8 | Users with permission 'eligible_for_leave' are shown in the list.
9 | A report summary page similar to the Timesheet Plugin shows the leaves summary.
10 | Leaves are also integrated into the Calendar Module.
11 | User who does not check-in, is automatically marked as off after some cut-off period Leave Types are defined through Redmine settings.
12 | User can also view the check-ins/check-outs and allowed users can edit attendance too.
13 | Also, attendance can be imported in the form of csv files.
14 |
15 | Redmine 3+ compatible.
16 |
17 | Installation:
18 | -------------
19 |
20 | - To install plugin, go to plugins folder of your Redmine repository and run:
21 |
22 | git clone http://github.com/arkhitech/redmine_leaves
23 |
24 | - Run db migrations for the plugin
25 |
26 | rake redmine:plugins:migrate RAILS_ENV=production
27 |
28 | - Run rake task to populate time_spent field in user_time_checks
29 |
30 | RAILS_ENV=production rake redmine_leaves:populate_time_spent
31 |
32 | - Bundle install all the gems using the following command
33 |
34 | bundle install
35 |
36 | - After bundle install, go to Redmine leave plugin folder and run the following command
37 |
38 | wheneverize .
39 |
40 | - Above command creates a file “schedule.rb” in config folder. Go to config/schedule.rb and type the following code
41 |
42 | every :weekday, :at => '12:01 pm' do
43 | rake "redmine_leaves:auto_leave_mark"
44 | end
45 |
46 | - Run the following command to automate the leave marking process
47 |
48 | whenever --update-crontab store
49 |
50 | - After installation, log in to Redmine as administrator and go to plugin settings for Redmine leaves plugin configuration.
51 |
52 | Instructions:
53 | -------------
54 |
55 | - After installing Redmine leave plugin, it is advised to make Redmine groups and add users into these groups. This will allow the admin to assign permissions to users accordingly.
56 |
57 | - Admin will be required to always update the "Auto-Mark" process whenever the server restarts. To do so, go to congif/schedule.rb and run the following command
58 |
59 | whenever --update-crontab store
60 |
61 | Snapshots:
62 | -------------
63 |
64 | 
65 | 
66 | 
67 | 
68 | 
69 | 
70 | 
71 | 
72 | 
73 | 
74 | 
75 | 
76 | 
77 | 
78 | 
79 | 
80 |
--------------------------------------------------------------------------------
/app/views/user_time_checks/user_time_activity_report.erb:
--------------------------------------------------------------------------------
1 | <%= render template: "layouts/user_time_analytics" %>
2 |
3 | User Time Activity Report
4 | <% if User.current.allowed_to_globally?(:view_time_reports,{}) -%>
5 |
6 | <%if @all_trackers%>
7 | <%= form_tag controller: 'user_time_checks', action: 'user_time_activity_report' do %>
8 | <%= t(:label_filter_plural) %>
9 |
10 | <%= "Date From:"%> <%= text_field_tag "date_from",
11 | (params[:date_from]) || Date.today - 1.month %>
12 | <%= calendar_for 'date_from' %>
13 |
14 | <%=" Date To:" %> <%= text_field_tag "date_to",
15 | (params[:date_to]) ||Date.today %>
16 | <%= calendar_for 'date_to' %>
17 | <%= submit_tag 'Apply' %>
18 |
19 |
20 |
21 |
94 |
95 | <% end %><%#form end%>
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | <%else%>
105 |
106 | No data to view
107 | Back
108 | <%end%>
109 |
110 |
111 |
112 | <%else%>
113 |
114 | You do not have permission to view reports
115 | Back
116 | <%end%>
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/spec/features/user_time_checks_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "UserTimeChecks" do
4 | #works for all users to get them login
5 | before (:all) do
6 | @user=User.find_by_login('testuser')
7 | @user ||= begin
8 | user = User.new(firstname: "test", lastname: "user", mail: "test@user.com")
9 | user.login = "testuser"
10 | user.password = "testuser"
11 | user.save!
12 | user
13 | end
14 |
15 | end
16 |
17 |
18 |
19 | describe "Check In Page" do
20 | #Used to check the Check-in and Refresh Functionality
21 | before :each do
22 | User.stubs(:current).returns(@user)
23 | end
24 |
25 | it "should have the content 'Check-in time:'" do
26 | visit '/user_time_checks/check_in'
27 | expect(page).to have_content('Check-in time:')
28 |
29 | end
30 | it "Should Display error flash message if Check-in is Refreshed [Pressed Twice]" do
31 | visit("/")
32 | click_link("CHECK-IN")
33 | visit '/user_time_checks/check_in'
34 | expect(page).to have_content('You did not check-out last time, please check-out first')
35 | end
36 | end
37 |
38 | describe "Check Out Page" do
39 |
40 | #Used to check the Check-out and Refresh Functionality
41 | before :each do
42 | User.stubs(:current).returns(@user)
43 |
44 | @user_time_check ||= begin
45 | user_time_check = UserTimeCheck.new(user_id: User.current.id, check_in_time: Time.now - 8.hours)
46 | user_time_check.save!
47 | user_time_check
48 | end
49 |
50 |
51 | end
52 |
53 | it "should have the content 'Check-Out'" do
54 | visit '/user_time_checks/check_out'
55 | expect(page).to have_content('Check-Out')
56 | end
57 |
58 | it "should have the content 'Check-out time:'" do
59 | visit '/user_time_checks/check_out'
60 | expect(page).to have_content('Check-out time:')
61 | end
62 |
63 | it "Should display error flash message if Check-out is Refreshed [Pressed Twice]" do
64 | visit("/")
65 | click_link("CHECK-OUT")
66 | visit '/user_time_checks/check_out'
67 | expect(page).to have_content('You did not check-in last time, please check-in first')
68 | end
69 |
70 | before do
71 | time_entries = TimeEntry.where(user_id: User.current.id ,
72 | spent_on: [@user_time_check.check_in_time, @user_time_check.check_in_time + 8.hours])
73 | time_entries.destroy_all
74 | end
75 |
76 | it "Should display error Flash meesage when Logged Time is less than Checked Time" do
77 | visit("/")
78 | click_link("CHECK-OUT")
79 | #visit '/user_time_checks/check_out'
80 | expect(page).to have_content('Your logged in time is less than required Percentage. Log your remaining time')
81 | click_button('Add')
82 | visit '/user_time_checks/checkout_timelog_success'
83 | end
84 |
85 |
86 |
87 | end
88 |
89 | # describe "Check Out Time Log Success" do
90 | #
91 | # before :each do
92 | # User.stubs(:current).returns(@user)
93 | #
94 | # @user_time_check ||= begin
95 | # user_time_check = UserTimeCheck.new(user_id: User.current.id, check_in_time: Time.now - 8.hours,check_out_time: Time.now+1.hours)
96 | # user_time_check.save!
97 | # user_time_check
98 | # end
99 | #
100 | # time_entries = TimeEntry.where(user_id: User.current.id ,
101 | # spent_on: [@user_time_check.check_in_time, @user_time_check.check_in_time + 8.hours])
102 | #
103 | # end
104 | #
105 | # it "Should display 'Time Logged Successfully' " do
106 | ## visit("/")
107 | ## click_link("CHECK-OUT")
108 | ## visit '/user_time_checks/check_out'
109 | ## render :action => "create_time_entries"
110 | ## click_button('Add')
111 | ## visit '/user_time_checks/checkout_timelog_success'
112 | # expect(view).to render_template("/user_time_checks/checkout_timelog_success")
113 | # expect(page).to have_content('Time Logged Successfully')
114 | #
115 | # end
116 | #
117 | # end
118 |
119 |
120 |
121 | end
122 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actionmailer (4.0.1)
5 | actionpack (= 4.0.1)
6 | mail (~> 2.5.4)
7 | actionpack (4.0.1)
8 | activesupport (= 4.0.1)
9 | builder (~> 3.1.0)
10 | erubis (~> 2.7.0)
11 | rack (~> 1.5.2)
12 | rack-test (~> 0.6.2)
13 | activemodel (4.0.1)
14 | activesupport (= 4.0.1)
15 | builder (~> 3.1.0)
16 | activerecord (4.0.1)
17 | activemodel (= 4.0.1)
18 | activerecord-deprecated_finders (~> 1.0.2)
19 | activesupport (= 4.0.1)
20 | arel (~> 4.0.0)
21 | activerecord-deprecated_finders (1.0.3)
22 | activesupport (4.0.1)
23 | i18n (~> 0.6, >= 0.6.4)
24 | minitest (~> 4.2)
25 | multi_json (~> 1.3)
26 | thread_safe (~> 0.1)
27 | tzinfo (~> 0.3.37)
28 | arel (4.0.1)
29 | atomic (1.1.14)
30 | builder (3.1.4)
31 | business_time (0.6.2)
32 | activesupport (>= 3.1.0)
33 | tzinfo (~> 0.3.31)
34 | celluloid (0.15.2)
35 | timers (~> 1.1.0)
36 | childprocess (0.3.6)
37 | ffi (~> 1.0, >= 1.0.6)
38 | chronic (0.10.2)
39 | coderay (1.1.0)
40 | diff-lcs (1.2.5)
41 | erubis (2.7.0)
42 | factory_girl (4.3.0)
43 | activesupport (>= 3.0.0)
44 | factory_girl_rails (4.3.0)
45 | factory_girl (~> 4.3.0)
46 | railties (>= 3.0.0)
47 | ffi (1.9.3)
48 | formatador (0.2.4)
49 | guard (2.2.4)
50 | formatador (>= 0.2.4)
51 | listen (~> 2.1)
52 | lumberjack (~> 1.0)
53 | pry (>= 0.9.12)
54 | thor (>= 0.18.1)
55 | guard-rspec (2.5.0)
56 | guard (>= 1.1)
57 | rspec (~> 2.11)
58 | guard-spork (1.5.0)
59 | childprocess (>= 0.2.3)
60 | guard (>= 1.1)
61 | spork (>= 0.8.4)
62 | hike (1.2.3)
63 | i18n (0.6.5)
64 | listen (2.2.0)
65 | celluloid (>= 0.15.2)
66 | rb-fsevent (>= 0.9.3)
67 | rb-inotify (>= 0.9)
68 | lumberjack (1.0.4)
69 | mail (2.5.4)
70 | mime-types (~> 1.16)
71 | treetop (~> 1.4.8)
72 | method_source (0.8.2)
73 | mime-types (1.25.1)
74 | minitest (4.7.5)
75 | multi_json (1.8.2)
76 | polyglot (0.3.3)
77 | pry (0.9.12.4)
78 | coderay (~> 1.0)
79 | method_source (~> 0.8)
80 | slop (~> 3.4)
81 | rack (1.5.2)
82 | rack-test (0.6.2)
83 | rack (>= 1.0)
84 | rails (4.0.1)
85 | actionmailer (= 4.0.1)
86 | actionpack (= 4.0.1)
87 | activerecord (= 4.0.1)
88 | activesupport (= 4.0.1)
89 | bundler (>= 1.3.0, < 2.0)
90 | railties (= 4.0.1)
91 | sprockets-rails (~> 2.0.0)
92 | railties (4.0.1)
93 | actionpack (= 4.0.1)
94 | activesupport (= 4.0.1)
95 | rake (>= 0.8.7)
96 | thor (>= 0.18.1, < 2.0)
97 | rake (10.1.0)
98 | rb-fsevent (0.9.3)
99 | rb-inotify (0.9.2)
100 | ffi (>= 0.5.0)
101 | rspec (2.13.0)
102 | rspec-core (~> 2.13.0)
103 | rspec-expectations (~> 2.13.0)
104 | rspec-mocks (~> 2.13.0)
105 | rspec-core (2.13.1)
106 | rspec-expectations (2.13.0)
107 | diff-lcs (>= 1.1.3, < 2.0)
108 | rspec-mocks (2.13.1)
109 | rspec-rails (2.13.1)
110 | actionpack (>= 3.0)
111 | activesupport (>= 3.0)
112 | railties (>= 3.0)
113 | rspec-core (~> 2.13.0)
114 | rspec-expectations (~> 2.13.0)
115 | rspec-mocks (~> 2.13.0)
116 | slop (3.4.7)
117 | spork (1.0.0rc4)
118 | spork-rails (4.0.0)
119 | rails (>= 3.0.0, < 5)
120 | spork (>= 1.0rc0)
121 | sprockets (2.10.1)
122 | hike (~> 1.2)
123 | multi_json (~> 1.0)
124 | rack (~> 1.0)
125 | tilt (~> 1.1, != 1.3.0)
126 | sprockets-rails (2.0.1)
127 | actionpack (>= 3.0)
128 | activesupport (>= 3.0)
129 | sprockets (~> 2.8)
130 | thor (0.18.1)
131 | thread_safe (0.1.3)
132 | atomic
133 | tilt (1.4.1)
134 | timers (1.1.0)
135 | treetop (1.4.15)
136 | polyglot
137 | polyglot (>= 0.3.1)
138 | tzinfo (0.3.38)
139 | whenever (>= 0.8.4)
140 | activesupport (>= 2.3.4)
141 | chronic (>= 0.6.3)
142 |
143 | PLATFORMS
144 | ruby
145 |
146 | DEPENDENCIES
147 | business_time
148 | childprocess
149 | factory_girl_rails
150 | guard-rspec
151 | guard-spork
152 | rspec-rails
153 | spork-rails
154 | whenever (>= 0.8.4)
155 |
--------------------------------------------------------------------------------
/app/views/user_time_checks/user_time_activity_report_monthly.erb:
--------------------------------------------------------------------------------
1 | <%= render template: "layouts/user_time_analytics" %>
2 |
3 | User Time Activity Monthly Report
4 | <% if User.current.allowed_to_globally?(:view_time_reports,{}) -%>
5 | <%if @all_trackers%>
6 | <%= form_tag controller: 'user_time_checks', action: 'user_time_activity_report_monthly' do %>
7 | <%= t(:label_filter_plural) %>
8 |
9 | <%= "Date From:"%> <%= text_field_tag "date_from",
10 | (params[:date_from]) || Date.today - 1.month %>
11 | <%= calendar_for 'date_from' %>
12 |
13 | <%=" Date To:" %> <%= text_field_tag "date_to",
14 | (params[:date_to]) ||Date.today %>
15 | <%= calendar_for 'date_to' %>
16 | <%= submit_tag 'Apply' %>
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | <%= "User Name " %>
27 | <%= "Missed due dates " %>
28 | <%= "Month " %>
29 | <%= "Year " %>
30 | <%# unless @trackers.nil? || @trackers.empty? %>
31 | <%# @trackers.each do |tracker| %>
32 | <% @all_trackers.each do |tracker|%>
33 |
34 | <%= " Num of "+tracker+" Handled"%>
35 |
36 | <%= "ACTUAL TIME FOR #{tracker}" %>
37 |
38 | <%= "Time Spent on "+tracker %>
39 | <%end%>
40 | <%#end%>
41 |
42 |
43 |
44 | <% unless @months_and_years.nil? || @months_and_years.empty? %>
45 | <% @months_and_years.each do |user| %>
46 |
47 |
48 | <%= user.user_id%>
49 |
50 |
51 | <% a = @missed_dates[user.user_id.to_s+user.month.to_s+user.year.to_s].first %>
52 | <% if a %>
53 | <%=+a.missed_dates %>
54 | <% else %>
55 | 0
56 | <% end %>
57 |
58 |
59 | <%= user.month%>
60 |
61 |
62 | <%= user.year%>
63 |
64 | <% @all_trackers.each do |tracker|%>
65 | <%# @trackers.each do |tracker| %>
66 |
67 |
68 | <% t = @time_spent_on_tracker[tracker+user.user_id.to_s+user.month.to_s+user.year.to_s].first %>
69 | <% if t %>
70 | <%=t.num_of_trackers %>
71 | <% else %>
72 | 0
73 | <% end %>
74 |
75 |
76 |
77 | <% if t %>
78 | <%= t.estimated_hours_on_tracker.round(2) %>
79 | <% else %>
80 | 0
81 | <% end %>
82 |
83 |
84 |
85 | <% if t %>
86 | <%= t.time_spent %>
87 | <% else %>
88 | 0
89 | <% end %>
90 |
91 | <%end%><%#end of all trackers loop%>
92 |
93 | <%end%>
94 | <% end %><%#end of unless%>
95 |
96 |
97 |
98 | <% end %>
99 |
100 |
101 |
102 |
103 | <%else%>
104 |
105 | No data to view
106 | Back
107 | <%end%>
108 |
109 |
110 |
111 | <%else%>
112 |
113 | You do not have permission to view reports
114 | Back
115 | <%end%>
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/app/controllers/user_leave_analytics_controller.rb:
--------------------------------------------------------------------------------
1 | class UserLeaveAnalyticsController < ApplicationController
2 | unloadable
3 |
4 | include UserLeaveAnalyticsHelper
5 | def report
6 |
7 | start_date = params[:user_leave_analytic] && params[:user_leave_analytic][:date_from].presence || Date.today.beginning_of_year
8 | end_date = params[:user_leave_analytic] && params[:user_leave_analytic][:date_to].presence || Date.today
9 | user_id = params[:user_leave_analytic] && params[:user_leave_analytic][:selected_user] || User.current.id
10 | @user = User.find(user_id)
11 | flash_message = ""
12 |
13 | @bar3 = LazyHighCharts::HighChart.new('graph') do |f|
14 | f.title({ text: "Overall Leaves Taken/Given - Bar Chart"})
15 | f.options[:xAxis][:categories] = ['Leave Type']
16 | all_leave_types.each do |leave|
17 | f.series(type: 'column',name: leave,data: [leaves_count_for(User.active, leave, start_date, end_date)])
18 | end
19 | end
20 | result = 0
21 | @bar3.series_data.each do |data|
22 | result += data[:data].first
23 | end
24 | flash_message ="No Results Found for Overall Leaves Taken/Given " if result==0
25 |
26 | @pie3 = LazyHighCharts::HighChart.new('pie') do |f|
27 | f.chart({defaultSeriesType: "pie" , margin: [50, 50, 50, 50]} )
28 | series = {
29 | type: 'pie',
30 | name: 'Group Leave Types',
31 | data: populate_pie_chart_for_user(User.active, start_date,end_date)
32 | }
33 | f.series(series)
34 | f.options[:title][:text] = "Overall Leaves Taken/Given - Pie Chart"
35 | f.legend(layout: 'vertical',style: {left: 'auto', bottom: 'auto',right: 'auto',top: '100px'})
36 | f.plot_options(pie:{
37 | allowPointSelect: true,
38 | cursor: "pointer" ,
39 | dataLabels:{
40 | enabled: true,
41 | color: "black",
42 | style:{
43 | font: "13px Trebuchet MS, Verdana, sans-serif"
44 | }
45 | }
46 | })
47 | end
48 |
49 | @bar2 = LazyHighCharts::HighChart.new('graph') do |f|
50 | f.title({ text: "Leaves taken by Groups - Bar Chart"})
51 | f.options[:xAxis][:categories] = Group.all.collect{|g| g.name}
52 | all_leave_types.each do |leave|
53 | f.series(type: 'column',name: leave,data: group_leaves(leave, start_date, end_date))
54 | end
55 | end
56 | result = 0
57 | @bar2.series_data.each do |data|
58 | result += data[:data].first
59 | end
60 | flash_message +="No Results Found for Leaves taken by Groups " if result==0
61 |
62 | @pie2 = LazyHighCharts::HighChart.new('pie') do |f|
63 | f.chart({defaultSeriesType: "pie" , margin: [50, 50, 50, 50]} )
64 | series = {
65 | type: 'pie',
66 | name: 'Group Leaves',
67 | data: populate_pie_chart_for_group(start_date,end_date)
68 | }
69 | f.series(series)
70 | f.options[:title][:text] = "Leaves taken by Groups - Pie Chart"
71 | f.legend(layout: 'vertical',style: {left: 'auto', bottom: 'auto',right: 'auto',top: '100px'})
72 | f.plot_options(pie:{
73 | allowPointSelect: true,
74 | cursor: "pointer",
75 | dataLabels:{
76 | enabled: true,
77 | color: "black",
78 | style:{
79 | font: "13px Trebuchet MS, Verdana, sans-serif"
80 | }
81 | }
82 | })
83 | end
84 |
85 | @bar1 = LazyHighCharts::HighChart.new('graph') do |f|
86 | f.title({ text: "Type of Leaves taken by #{@user.name} - Bar Chart"})
87 | f.options[:xAxis][:categories] = ['Leave Type']
88 |
89 | all_leave_types.each do |leave|
90 | f.series(type: 'column',name: leave,data: [leaves_count_for(@user.id, leave, start_date, end_date)])
91 | end
92 | end
93 | result = 0
94 | @bar1.series_data.each do |data|
95 | result += data[:data].first
96 | end
97 | flash_message +="No Results Found for Type of Leaves taken by #{@user.name}" if result==0
98 |
99 | @pie1 = LazyHighCharts::HighChart.new('pie') do |f|
100 | f.chart({defaultSeriesType: "pie" , margin: [50, 50, 50, 50]} )
101 | series = {
102 | type: 'pie',
103 | name: 'User Leave Types',
104 | data: populate_pie_chart_for_user(@user.id, start_date,end_date)
105 | }
106 | f.series(series)
107 | f.options[:title][:text] = "Type of Leaves taken by #{@user.name} - Pie Chart"
108 | f.legend(layout: 'vertical',style: {left: 'auto', bottom: 'auto',right: 'auto',top: '100px'})
109 | f.plot_options(pie:{
110 | allowPointSelect: true,
111 | cursor: "pointer" ,
112 | dataLabels:{
113 | enabled: true,
114 | color: "black",
115 | style:{
116 | font: "13px Trebuchet MS, Verdana, sans-serif"
117 | }
118 | }
119 | })
120 | end
121 | flash.now[:warning] = flash_message unless flash_message.blank?
122 | end
123 | end
124 |
--------------------------------------------------------------------------------
/app/views/settings/_settings.html.erb:
--------------------------------------------------------------------------------
1 |
2 | Leave Type Settings
3 |
4 |
5 |
6 |
7 | <%= t(:label_leave_types)%>
8 | <%= text_field_tag('settings[leave_types]', settings['leave_types']) %>
9 | <%= t(:label_multiple_leaves_instructions )%>
10 |
11 |
12 |
13 |
14 |
15 |
16 | <%= t(:label_default_type ) %>
17 |
18 | <%= text_field_tag('settings[default_type]', settings['default_type']) %>
19 |
20 |
21 |
22 |
23 |
24 | <%= t(:label_eligible_for_leave_groups ) %>
25 |
26 |
27 | <%= select_tag('settings[eligible_for_leave_groups]',
28 | options_from_collection_for_select(Group.all, :id, :name,
29 | settings['eligible_for_leave_groups']),
30 | multiple: true) %>
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | Permission Settings
42 |
43 |
44 |
45 |
46 | Mark
47 |
48 |
49 | <%= t(:label_mark_leaves ) %>
50 |
51 | <%= select_tag('settings[mark_leaves]',
52 | options_from_collection_for_select(Group.all, :id, :name,
53 | settings['mark_leaves']),
54 | multiple: true) %>
55 |
56 |
57 |
58 |
59 | <%= t(:label_mark_own_leave ) %>
60 |
61 |
62 | <%= select_tag('settings[mark_own_leave]',
63 | options_from_collection_for_select(Group.all, :id, :name,
64 | settings['mark_own_leave']),
65 | multiple: true) %>
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | Edit
75 |
76 |
77 | <%= t(:label_edit_attendance) %>
78 |
79 | <%= select_tag('settings[edit_attendance]',
80 | options_from_collection_for_select(Group.all, :id, :name,
81 | settings['edit_attendance']),
82 | multiple: true) %>
83 |
84 |
85 |
86 |
87 | <%= t(:label_edit_own_attendance) %>
88 |
89 |
90 | <%= select_tag('settings[edit_own_attendance]',
91 | options_from_collection_for_select(Group.all, :id, :name,
92 | settings['edit_own_attendance']),
93 | multiple: true) %>
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | Reminder Settings
106 |
107 |
108 |
109 | <%= t(:label_daily_mail_reminder_cc) %>
110 |
111 | <%= select_tag('settings[daily_reminder]',
112 | options_from_collection_for_select(Group.all, :id, :name,
113 | settings['daily_reminder']),
114 | multiple: true) %>
115 |
116 |
117 |
118 | <%= t(:label_weekly_mail_reminder_cc) %>
119 |
120 | <%= select_tag('settings[weekly_reminder]',
121 | options_from_collection_for_select(Group.all, :id, :name,
122 | settings['weekly_reminder']),
123 | multiple: true) %>
124 |
125 |
126 |
127 | <%= t(:label_monthly_mail_reminder_cc) %>
128 |
129 | <%= select_tag('settings[monthly_reminder]',
130 | options_from_collection_for_select(Group.all, :id, :name,
131 | settings['monthly_reminder']),
132 | multiple: true) %>
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 | Reminder Settings
141 |
142 | Cc'd Project Roles:
143 | <%= select_tag('settings[cc_roles]',
144 | options_for_select(Role.all.collect{|r| [r.name, r.id]},
145 | @settings['cc_roles'])) %>
146 |
147 |
148 |
149 |
150 | Time Activity Report Settings
151 |
152 |
153 |
154 |
155 | <%= t(:label_trackers)%>
156 | <%= text_field_tag('settings[tracker_names]', settings['tracker_names']) %>
157 | <%= t(:label_tracker_instructions )%>
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | Time Activity Report Settings
167 |
168 | Time Loggers Group
169 | <%= select_tag('settings[time_loggers_group]',
170 | options_from_collection_for_select(Group.all, :id, :name,
171 | settings['time_loggers_group']),
172 | multiple: false) %>
173 |
174 |
175 |
176 | Time Log Receivers Group
177 | <%= select_tag('settings[time_log_receivers_group]',
178 | options_from_collection_for_select(Group.all, :id, :name,
179 | settings['time_log_receivers_group']),
180 | multiple: false) %>
181 |
182 |
183 |
184 | Cc'd Project Roles:
185 | <%= select_tag('settings[cc_roles]',
186 | options_for_select(Role.all.collect{|r| [r.name, r.id]},
187 | @settings['cc_roles'])) %>
188 |
189 |
190 |
191 | Minimum Working Hours
192 | <%= select_tag('settings[num_min_working_hours]',
193 | options_for_select(0..10, settings['num_min_working_hours'].to_i)) %>
194 |
195 |
196 |
197 | Max Past Timelog Insert Days
198 | <%= select_tag('settings[max_past_timelog_insert_days]',
199 | options_for_select(0..30, @settings['max_past_timelog_insert_days'].to_i)) %>
200 |
201 |
202 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # English strings go here for Rails i18n
2 | en:
3 | #pagination in redmine time checks
4 | wice_grid:
5 | previous_label: «
6 | next_label: »
7 |
8 | #Main Menu
9 | caption_top_menu_check_in: "CHECK-IN"
10 | caption_top_menu_check_out: "CHECK-OUT"
11 | caption_leave_report: "Leaves"
12 | caption_user_time_check: "User Time Check"
13 |
14 | #user_leave_report/index
15 | title_leave_summary: " Leave Summary"
16 | title_leave_analytics: "Leave Analytics"
17 |
18 | #user_leave_report/_options
19 | link_add_leave: "Add Leave"
20 | label_date_from: "From"
21 | label_date_to: "To"
22 | label_sortby: "Sort By"
23 | label_user: "User"
24 | label_leave_types: "Leave Types"
25 | label_group: "Group"
26 | label_button_apply: "Apply"
27 | options_group_by_title: "Leave Type"
28 | option_group_by_user: "User"
29 | option_group_by_date: "Date"
30 |
31 | #user_leave_report/report
32 | label_name: "User Name"
33 | label_leave_type: "Leave Type"
34 | label_leave_date: "Leave Date"
35 | label_weight: "Weight"
36 | label_comments: "Comments"
37 | label_action: "Action"
38 | label_confirm: "Are you sure?"
39 | link_edit: "Edit"
40 | link_delete: "Delete"
41 |
42 | #user_leaves/new
43 | label_add_leave: "Add Leave"
44 | label_user_group: "User/Group"
45 | label_optional: "(Optional)"
46 | label_fractional_leave: "Fractional Leave"
47 | label_fractional_leave_optional: "(Optional:only required for half leaves or earned leaves [use negative value for earned])"
48 | label_button_add: "Add"
49 |
50 | #user_leaves/destroy
51 | label_deleted: "Leave has been deleted successfully!"
52 | #user_leaves/edit
53 | label_update_leave: "Update Leave"
54 | label_button_update: "Update"
55 |
56 | #leave_mailer/notify_absentee.html/.txt
57 | text_notify_your_leave_for: "%{fraction} %{leave_type}(%{total_yearly_leaves}) leave on %{leave_date} has been marked for %{user_name} with comments: %{comments}"
58 | text_notify_your_leave_for_html: "%{fraction} %{leave_type}(%{total_yearly_leaves}) leave on %{leave_date} has been marked for %{user_name} with comments:%{comments} "
59 |
60 |
61 | #log_time_reminder_mailer/reminder_email.html/.txt
62 | text_email_remainder_time: "Time Log Reminder for Issue# "
63 | text_email_assignee: "Assignee:"
64 | text_email_issue_inform: "This is to inform you that you have not logged time for Issue# "
65 | text_email_log_request: "Kindly log time for this issue."
66 |
67 | #settings/
68 | label_leave_types: "Leave Type(s)"
69 | label_multiple_leaves_instructions: " For multiple Leave Types, enter values separated by comma (,)"
70 | label_default_type: "Default Type"
71 | label_eligible_for_leave_groups: "Eligible For Leave Group(s)"
72 | label_mark_leaves: "Mark Leaves"
73 | label_mark_own_leave: "Mark Own Leave"
74 | label_daily_mail_reminder_cc: "Daily Mail Reminder CC"
75 | label_weekly_mail_reminder_cc: "Weekly Mail Reminder CC"
76 | label_monthly_mail_reminder_cc: "Monthly Mail Reminder CC"
77 | label_edit_attendance: "Edit Attendance"
78 | label_edit_own_attendance: "Edit Own Attendance"
79 |
80 | #user_timecheks/index.html
81 | label_attendance_summary: "Attendance Summary"
82 | label_import_csv: "Import CSV"
83 | label_button_import: "Import"
84 | label_file_note: "NOTE: File should be headerless, tab separated and column sequence should be [S.No. DATE Check-In Check-Out NAME ...]"
85 | error_no_records_found: "No Records Found!"
86 | label_check_in_time: "Check-in Time"
87 | label_check_out_time: "Check-out Time"
88 | label_spent_time: "Spent Time"
89 | label_not_allowed: "Not Allowed"
90 |
91 | #user_timecheks/_availible_open_issues
92 | label_issue_no: "Issue # "
93 | label_tracker: "Tracker"
94 | label_status: "Status"
95 | label_subject: "Subject"
96 | label_hours_to_log: "Hours To Log"
97 | label_activity: "Activity"
98 | label_comments: "Comments"
99 | error_post_save: "Prohibited this post from being saved:"
100 |
101 | #user_timechecks/_check_out_info
102 | label_time_spent: "Time Spent: "
103 |
104 | #user_timechecks/_show_logged_time_entries
105 | label_time_logged: "Time Logged"
106 | label_activity_to_log: "Activity to log"
107 |
108 |
109 | #user_timechecks/check_in
110 | label_check_in: "Check-In"
111 | label_current_user_id: "Current User ID:"
112 | label_current_user_name: "Current User Name:"
113 |
114 | #user_timechecks/check_out
115 | label_total_checked_time_is: "Your Total Checked Time is"
116 | label_total_logged_time_is: "Your Total Logged Time is"
117 | label_hours_for_today: "Hours For Today"
118 | label_please_log_remaining: "Please Log Remaining"
119 |
120 |
121 | #user_timechecks/checkout_timelog_success
122 | label_time_logged_success: "Time Logged Successfully"
123 | label_check_out: "Check-Out"
124 |
125 | #user_timecheks/ edit
126 |
127 |
128 | #CONTROLLERS
129 | #user_leave_reports_controller
130 | error_no_params: "No parameters selected"
131 | error_invalid_date: "Invalid Date Format"
132 | error_no_results: "No Results Found!"
133 | error_from_greater_than_to: "From date can not be greater than to Date"
134 |
135 | #user_leaves_controller
136 | notice_leave_add: "%{leave_type}(%{total_yearly_leaves}) leave marked for %{user_name} for %{leave_date}"
137 | error_leave_add: "%{leave_type} leave for %{user_name} for %{leave_date} could not be added. Reason: %{reason}"
138 |
139 | error_no_user_group_selected: "No User/Group Selected!"
140 | error_invalid_user_group_selected: "Invalid User/Group Selected!"
141 | notice_leaves_updated: "User Leave Updated!"
142 | error_leaves_not_updated: "User Leave not Updated!"
143 |
144 | #user_time_checks_controller
145 | notice_time_check_updated: "User Time Check Updated"
146 | notice_time_check_file_imported: "User Time Checks Imported"
147 |
148 | error_invalid_file_format: "Invalid File Format!"
149 | error_checkout_first: "You did not check-out last time, please check-out first"
150 | error_checkin_first: "You did not check-in last time, please check-in first"
151 | error_less_time_logged: "Your logged in time is less than required Percentage. Log your remaining time"
152 |
153 | #MODELS
154 | #leave_mailer
155 | subject_leave_marked_for: "Leave - %{user_name} - %{fraction} %{leave_type}(%{total_yearly_leaves}) %{leave_date}"
156 | subject_project_timesheet: "Daily Timesheet Report - %{project} - %{report_date}"
157 | subject_group_timesheet: "Daily Timesheet Report - %{group} - %{report_date}"
158 |
159 | subject_missing_time_log: "Missing Time Log - %{user_name} - %{report_date}"
160 |
161 | body_time_log_hours_not_logged_html: "You have not logged your time for %{report_date}"
162 | body_time_log_hours_missing_html: "You have only logged %{logged_hours} for %{report_date}"
163 | body_affirm_missing_time_log_html: "Please update your time log to ensure time is logged completely. You will not be able to log time after three days and your leave will be marked."
164 | footer_missing_time_log: "Thank you for your co-operation."
165 | #log_time_remainder_mailer
166 | label_reminder_for_time_log: "Time Log Reminder for Issue#"
167 |
168 | #user_leave
169 | error_fractional_value_greater_than_one: ": Fractional Leave value can't be grater than 1"
170 | error_fractional_value_less_than_zero: ": Fractional Leave value can't be less than 0"
171 |
172 | #user_time_check
173 | auto_generated_comment: "Auto-Generated Check-Out"
174 | field_user_id: "User name"
175 | field_user_name: "User Name"
176 | field_check_out_time: "Check Out Time"
177 | field_check_in_time: "Check In Time"
178 | field_time_spent: "Time Spent"
179 |
180 | label_Timeentry_plural : "User Time Entries"
181 | label_Timereport: "User Time Report"
182 | label_filters: "Filters"
183 | label_trackers: "Trackers"
184 | label_tracker_instructions: "Enter trackers to display,separated by comma(,)"
--------------------------------------------------------------------------------
/assets/stylesheets/wice_grid.css:
--------------------------------------------------------------------------------
1 | /* line 9, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
2 | .wg-detached-filter .text-filter-container input, .wice-grid .text-filter-container input {
3 | width: auto;
4 | margin-right: 10px;
5 | display: inline;
6 | }
7 | /* line 16, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves//stylesheets/wice_grid.css.scss */
8 | .wg-detached-filter a.date-label, .wice-grid a.date-label {
9 | text-decoration: none;
10 | }
11 | /* line 17, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
12 | .wg-detached-filter a.date-label:hover, .wice-grid a.date-label:hover {
13 | text-decoration: line-through;
14 | }
15 | /* line 19, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
16 | .wg-detached-filter .clickable, .wice-grid .clickable {
17 | cursor: pointer;
18 | margin-bottom: 2px;
19 | margin-right: 2px;
20 | }
21 | /* line 25, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
22 | .wg-detached-filter .ui-datepicker-trigger, .wg-detached-filter .wg-detached-filter .ui-datepicker-trigger, .wice-grid .ui-datepicker-trigger, .wice-grid .wg-detached-filter .ui-datepicker-trigger {
23 | cursor: pointer;
24 | }
25 | /* line 31, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
26 | .wg-detached-filter .custom-dropdown-container .expand-multi-select-icon, .wg-detached-filter .custom-dropdown-container .collapse-multi-select-icon, .wice-grid .custom-dropdown-container .expand-multi-select-icon, .wice-grid .custom-dropdown-container .collapse-multi-select-icon {
27 | width: 10px;
28 | height: 10px;
29 | display: inline-block;
30 | margin-left: 5px;
31 | vertical-align: top;
32 | }
33 | /* line 39, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
34 | .wg-detached-filter .custom-dropdown-container .collapse-multi-select-icon, .wice-grid .custom-dropdown-container .collapse-multi-select-icon {
35 | background: transparent url(/plugin_assets/redmine_leaves/images/icons/grid/collapse.gif) no-repeat;
36 | }
37 | /* line 43, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
38 | .wg-detached-filter .custom-dropdown-container .expand-multi-select-icon, .wice-grid .custom-dropdown-container .expand-multi-select-icon {
39 | background: transparent url(/plugin_assets/redmine_leaves/images/icons/grid/expand.gif) no-repeat;
40 | }
41 |
42 | /* line 50, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
43 | .wice-grid {
44 | background-color: #D1D0CE;
45 | /* in case of twitter bootstrap :) */
46 | }
47 | /* line 53, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
48 | .wice-grid .desc, .wice-grid .asc {
49 | padding-right: 18px;
50 | text-decoration: none;
51 | }
52 | /* line 58, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
53 | .wice-grid .desc {
54 | background: transparent url(/plugin_assets/redmine_leaves/images/icons/grid/arrow_down.gif) right no-repeat;
55 | }
56 | /* line 61, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
57 | .wice-grid .asc {
58 | background: transparent url(/plugin_assets/redmine_leaves/images/icons/grid/arrow_up.gif) right no-repeat;
59 | }
60 | /* line 65, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
61 | .wice-grid .submit.clickable {
62 | background: transparent url(/plugin_assets/redmine_leaves/images/icons/grid/table_refresh.png) no-repeat;
63 | width: 50px;
64 | height: 20px;
65 | float: left;
66 | margin-left: -100%;
67 | }
68 |
69 | .wice-grid .ui-datepicker-trigger{
70 | background: transparent url(/plugin_assets/redmine_leaves/images/icons/grid/calendar_view_month.png) no-repeat;
71 | width: 50px;
72 | height: 16px;
73 | padding: 0px 17px 2px 20px;
74 | }
75 | /* line 70, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
76 | .wice-grid .reset.clickable {
77 | background: transparent url(/plugin_assets/redmine_leaves/images/icons/grid/table.png) no-repeat;
78 | width: 50px;
79 | height: 20px;
80 | }
81 | /* line 77, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
82 | .wice-grid .wg-show-filter.clickable,
83 | .wice-grid .wg-hide-filter.clickable {
84 | background: transparent url(/plugin_assets/redmine_leaves/images/icons/grid/page_white_find.png) no-repeat;
85 | width: 16px;
86 | height: 16px;
87 | }
88 | /* line 83, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
89 | .wice-grid .export-to-csv-button.clickable {
90 | background: transparent url(/plugin_assets/redmine_leaves/images/icons/grid/page_white_excel.png) no-repeat;
91 | width: 16px;
92 | height: 16px;
93 | }
94 | /* line 88, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
95 | .wice-grid .clickable.select-all {
96 | background: transparent url(/plugin_assets/redmine_leaves/images/icons/grid/tick_all.png) no-repeat;
97 | width: 16px;
98 | height: 16px;
99 | float: left;
100 | }
101 | /* line 94, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
102 | .wice-grid .clickable.deselect-all {
103 | background: transparent url(/plugin_assets/redmine_leaves/images/icons/grid/untick_all.png) no-repeat;
104 | width: 16px;
105 | height: 16px;
106 | float: left;
107 | }
108 | /* line 102, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
109 | .wice-grid thead th select {
110 | width: auto;
111 | }
112 | /* line 106, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
113 | .wice-grid .my_pagination {
114 | margin: 0px;
115 | float: left;
116 | }
117 | /* line 112, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
118 | .wice-grid tr.wg-filter-row input[type=text] {
119 | width: 100px;
120 | }
121 | /* line 116, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
122 | .wice-grid .pagination_status {
123 | font-weight: bold;
124 | float: right;
125 | }
126 |
127 | /* line 124, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
128 | .wice-grid-query-panel li {
129 | list-style-type: none;
130 | }
131 | /* line 126, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
132 | .wice-grid-query-panel ul {
133 | margin-left: 0;
134 | }
135 | /* line 128, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
136 | .wice-grid-query-panel a.wice-grid-delete-query .delete-icon {
137 | background: transparent url(/plugin_assets/redmine_leaves/images/icons/grid/delete.png) no-repeat;
138 | float: left;
139 | width: 16px;
140 | height: 16px;
141 | margin-left: 0px;
142 | }
143 |
144 | /* line 135, /home/arkhitech/Desktop/projects/reality_tours/app/plugin_assets/redmine_leaves/stylesheets/wice_grid.css.scss */
145 | input.wice-grid-save-query-field {
146 | width: auto;
147 | display: inline-block;
148 | margin-right: -128px;
149 | display: block;
150 | text-align: justify;
151 |
152 | }
153 | .wice-grid-save-query-button{
154 | display: block;
155 | padding: 2px;
156 | text-align: justify;
157 | margin-top: 6px;
158 | }
159 |
160 | li {
161 | line-height: 18px;
162 | }
163 |
164 | #export-button {
165 | float: right;
166 | margin-left: 5px;
167 | }
168 |
--------------------------------------------------------------------------------
/lib/wice_grid_config.rb:
--------------------------------------------------------------------------------
1 | if defined?(Wice::Defaults)
2 |
3 | # Default number of rows to show per page.
4 | Wice::Defaults::PER_PAGE = 20
5 |
6 | # Default order direction
7 | Wice::Defaults::ORDER_DIRECTION = 'asc'
8 |
9 | # Default name for a grid. A grid name is the basis for a lot of
10 | # names including parameter names, DOM IDs, etc
11 | # The shorter the name is the shorter the request URI will be.
12 | Wice::Defaults::GRID_NAME = 'grid'
13 |
14 | # If REUSE_LAST_COLUMN_FOR_FILTER_ICONS is true and the last column doesn't have any filter and column name, it will be used
15 | # for filter related icons (filter icon, reset icon, show/hide icon), otherwise an additional table column is added.
16 | Wice::Defaults::REUSE_LAST_COLUMN_FOR_FILTER_ICONS = true
17 |
18 | # The label of the first option of a custom dropdown list meaning 'All items'
19 | Wice::Defaults::CUSTOM_FILTER_ALL_LABEL = '--'
20 |
21 | # A list of classes for the table tag of the grid
22 | Wice::Defaults::DEFAULT_TABLE_CLASSES = ['table', 'table-bordered', 'table-striped']
23 |
24 | # Allow switching between a single and multiple selection modes in custom filters (dropdown boxes)
25 | Wice::Defaults::ALLOW_MULTIPLE_SELECTION = true
26 |
27 | # Show the upper pagination panel by default or not
28 | Wice::Defaults::SHOW_UPPER_PAGINATION_PANEL = false
29 |
30 | # Disabling CSV export by default
31 | Wice::Defaults::ENABLE_EXPORT_TO_CSV = false
32 |
33 | # Default CSV field separator
34 | Wice::Defaults::CSV_FIELD_SEPARATOR = ','
35 |
36 | # Default CSV encoding (p.e. 'CP1252:UTF-8' to make Microsoft Excel(tm) happy)
37 | Wice::Defaults::CSV_ENCODING = nil
38 |
39 | # The strategy when to show the filter.
40 | # * :when_filtered - when the table is the result of filtering
41 | # * :always - show the filter always
42 | # * :no - never show the filter
43 | Wice::Defaults::SHOW_FILTER = :always
44 |
45 | # A boolean value specifying if a change in a filter triggers reloading of the grid.
46 | Wice::Defaults::AUTO_RELOAD = false
47 |
48 | # SQL operator used for matching strings in string filters.
49 | Wice::Defaults::STRING_MATCHING_OPERATOR = 'LIKE'
50 | # STRING_MATCHING_OPERATOR = 'ILIKE' # Use this for Postgresql case-insensitive matching.
51 |
52 | # Defining one string matching operator globally for the whole application turns is not enough
53 | # when you connect to two databases one of which is MySQL and the other is Postgresql.
54 | # If the key for an adapter is missing it will fall back to Wice::Defaults::STRING_MATCHING_OPERATOR.
55 | #
56 | # 'CI_LIKE' is a special value. Setting a value in STRING_MATCHING_OPERATORS to CI_LIKE will result in the following SQL:
57 | #
58 | # UPPER(table.field) LIKE UPPER(?)"
59 | Wice::Defaults::STRING_MATCHING_OPERATORS = {
60 | 'ActiveRecord::ConnectionAdapters::MysqlAdapter' => 'LIKE',
61 | 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter' => 'ILIKE'
62 | }
63 |
64 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
65 | # Advanced Filters #
66 |
67 | # Switch of the negation checkbox in all text filters
68 | Wice::Defaults::NEGATION_IN_STRING_FILTERS = false
69 |
70 | # Each WiceGrid filter column is defined in two classes, one used for rendering the filter, the other
71 | # for generating query conditions. All these columns are in lib/wice/columns/*.rb .
72 | # File lib/wice/columns/column_processor_index.rb lists all predefined processors.
73 | # In most cases a processor is chosen automatically based on the DB column type,
74 | # for example, integer columns
75 | # can have two of processors, the default one with one input field, and a processor called "range",
76 | # with 2 input fields. In this case it is possible to specify a processor in the column definition:
77 | #
78 | # g.column filter_type: :range
79 | #
80 | # It is also possible to define you own processors:
81 | #
82 | # Wice::Defaults::ADDITIONAL_COLUMN_PROCESSORS = {
83 | # some_key_identifying_new_column_type: ['AViewColumnProcessorClass', 'ConditionsGeneratorClass'],
84 | # another_key_identifying_new_column_type: ['AnotherViewColumnProcessorClass', 'AnotherConditionsGeneratorClass']
85 | # }
86 | #
87 | # Column processor keys/names should not coincide with the existing keys/names (see lib/wice/columns/column_processor_index.rb)
88 | # the value is a 2-element array with 2 strings, the first should be a name of view processor class inherited from
89 | # Wice::Columns::ViewColumn, the second should be a name of conditions generator class inherited from
90 | # Wice::Columns::ConditionsGeneratorColumn .
91 |
92 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
93 | # Showing All Records #
94 |
95 | # Enable or disable showing all records (non-paginated table)
96 | Wice::Defaults::ALLOW_SHOWING_ALL_RECORDS = true
97 |
98 | # If number of all queries is more than this value, the user will be given a warning message
99 | Wice::Defaults::START_SHOWING_WARNING_FROM = 100
100 |
101 | # Hide the "show all" link if the number of all records is more than...
102 | # Force-resets back to pagination starting from this value.
103 | # Set to nil to always show it
104 | Wice::Defaults::SHOW_ALL_ALLOWED_UP_TO = nil
105 |
106 | #
107 | # set to nil to skip the check
108 | Wice::Defaults::SWITCH_BACK_TO_PAGINATION_FROM = nil
109 |
110 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
111 | # Saving Queries #
112 |
113 | # ActiveRecord model to store queries. Read the documentation for details
114 | # QUERY_STORE_MODEL = 'WiceGridSerializedQuery'
115 | Wice::Defaults::QUERY_STORE_MODEL = 'WiceGridSerializedQuery'
116 |
117 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
118 | # Here go settings related to the date/datetime filters #
119 |
120 | # Default column filters
121 | # Possible values:
122 | # * :jquery_datepicker - Jquery datepicker (works for datetime, too)
123 | # * :bootstrap_datepicker - Bootstrap datepicker (works for datetime, too)
124 | # * :rails_date_helper - standard Rails date helper
125 | # * :rails_datetime_helper - standard Rails datetime helper
126 |
127 | Wice::Defaults::DEFAULT_FILTER_FOR_DATE = :jquery_datepicker
128 | Wice::Defaults::DEFAULT_FILTER_FOR_DATETIME = :jquery_datepicker
129 |
130 | # Format of the datetime displayed.
131 | # If you change the format, make sure to check if +DATETIME_PARSER+ can still parse this string.
132 | Wice::Defaults::DATETIME_FORMAT = '%Y-%m-%d %H:%M'
133 |
134 | # Format of the date displayed.
135 | # If you change the format, make sure to check if +DATE_PARSER+ can still parse this string.
136 | Wice::Defaults::DATE_FORMAT = '%Y-%m-%d'
137 |
138 | # Format of the date displayed in jQuery's Datepicker
139 | # If you change the format, make sure to check if +DATE_PARSER+ can still parse this string.
140 | Wice::Defaults::DATE_FORMAT_JQUERY = 'yy-mm-dd'
141 |
142 | # Format of the date displayed in Bootstrap's Datepicker
143 | # If you change the format, make sure to check if +DATE_PARSER+ can still parse this string.
144 | Wice::Defaults::DATE_FORMAT_BOOTSTRAP = 'yyyy-mm-dd'
145 |
146 | # With Calendar helpers enabled the parameter sent is the string displayed. This lambda will be given a date string in the
147 | # format defined by +DATETIME_FORMAT+ and must generate a DateTime object.
148 | # In many cases Time.zone.parse is enough, for instance, %Y-%m-%d . If you change the format, make sure to check this code
149 | # and modify it if needed.
150 | Wice::Defaults::DATETIME_PARSER = lambda do|datetime_string|
151 | if datetime_string.blank?
152 | nil
153 | elsif Time.zone
154 | Time.zone.parse(datetime_string)
155 | else
156 | Time.parse(datetime_string)
157 | end
158 | end
159 |
160 | # The range of years to display in jQuery Datepicker.
161 | # It can always be changed dynamically with the following javascript:
162 | # $( ".hasDatepicker" ).datepicker( "option", "yearRange", "2000:2042" );
163 | Wice::Defaults::DATEPICKER_YEAR_RANGE = (from = Date.current.year - 10).to_s + ':' + (from + 15).to_s
164 |
165 | # With Calendar helpers enabled the parameter sent is the string displayed. This lambda will be given a date string in the
166 | # format defined by +DATETIME+ and must generate a Date object.
167 | # In many cases Date.parse is enough, for instance, %Y-%m-%d . If you change the format, make sure to check this code
168 | # and modify it if needed.
169 | Wice::Defaults::DATE_PARSER = lambda do|date_string|
170 | if date_string.blank?
171 | nil
172 | else
173 | begin
174 | Date.parse(date_string)
175 | rescue ArgumentError
176 | nil
177 | end
178 | end
179 | end
180 |
181 | # The name of the page method (should correspond to Kaminari.config.page_method_name)
182 | Wice::Defaults::PAGE_METHOD_NAME = :page
183 |
184 | # The name of the theme to use for the pagination with Kaminari
185 | Wice::Defaults::PAGINATION_THEME = :wice_grid
186 |
187 | # By default ActiveRecord calls are always executed inside Model.unscoped{}.
188 | # Setting USE_DEFAULT_SCOPE to true will use the default scope for all queries.
189 | Wice::Defaults::USE_DEFAULT_SCOPE = false
190 |
191 | end
192 |
--------------------------------------------------------------------------------
/assets/javascripts/jquery_ujs.js:
--------------------------------------------------------------------------------
1 | (function($, undefined) {
2 |
3 | /**
4 | * Unobtrusive scripting adapter for jQuery
5 | * https://github.com/rails/jquery-ujs
6 | *
7 | * Requires jQuery 1.7.0 or later.
8 | *
9 | * Released under the MIT license
10 | *
11 | */
12 |
13 | // Cut down on the number of issues from people inadvertently including jquery_ujs twice
14 | // by detecting and raising an error when it happens.
15 | if ( $.rails !== undefined ) {
16 | $.error('jquery-ujs has already been loaded!');
17 | }
18 |
19 | // Shorthand to make it a little easier to call public rails functions from within rails.js
20 | var rails;
21 | var $document = $(document);
22 |
23 | $.rails = rails = {
24 | // Link elements bound by jquery-ujs
25 | linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]',
26 |
27 | // Button elements bound by jquery-ujs
28 | buttonClickSelector: 'button[data-remote]',
29 |
30 | // Select elements bound by jquery-ujs
31 | inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]',
32 |
33 | // Form elements bound by jquery-ujs
34 | formSubmitSelector: 'form',
35 |
36 | // Form input elements bound by jquery-ujs
37 | formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])',
38 |
39 | // Form input elements disabled during form submission
40 | disableSelector: 'input[data-disable-with], button[data-disable-with], textarea[data-disable-with]',
41 |
42 | // Form input elements re-enabled after form submission
43 | enableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled',
44 |
45 | // Form required input elements
46 | requiredInputSelector: 'input[name][required]:not([disabled]),textarea[name][required]:not([disabled])',
47 |
48 | // Form file input elements
49 | fileInputSelector: 'input[type=file]',
50 |
51 | // Link onClick disable selector with possible reenable after remote submission
52 | linkDisableSelector: 'a[data-disable-with]',
53 |
54 | // Make sure that every Ajax request sends the CSRF token
55 | CSRFProtection: function(xhr) {
56 | var token = $('meta[name="csrf-token"]').attr('content');
57 | if (token) xhr.setRequestHeader('X-CSRF-Token', token);
58 | },
59 |
60 | // making sure that all forms have actual up-to-date token(cached forms contain old one)
61 | refreshCSRFTokens: function(){
62 | var csrfToken = $('meta[name=csrf-token]').attr('content');
63 | var csrfParam = $('meta[name=csrf-param]').attr('content');
64 | $('form input[name="' + csrfParam + '"]').val(csrfToken);
65 | },
66 |
67 | // Triggers an event on an element and returns false if the event result is false
68 | fire: function(obj, name, data) {
69 | var event = $.Event(name);
70 | obj.trigger(event, data);
71 | return event.result !== false;
72 | },
73 |
74 | // Default confirm dialog, may be overridden with custom confirm dialog in $.rails.confirm
75 | confirm: function(message) {
76 | return confirm(message);
77 | },
78 |
79 | // Default ajax function, may be overridden with custom function in $.rails.ajax
80 | ajax: function(options) {
81 | return $.ajax(options);
82 | },
83 |
84 | // Default way to get an element's href. May be overridden at $.rails.href.
85 | href: function(element) {
86 | return element.attr('href');
87 | },
88 |
89 | // Submits "remote" forms and links with ajax
90 | handleRemote: function(element) {
91 | var method, url, data, elCrossDomain, crossDomain, withCredentials, dataType, options;
92 |
93 | if (rails.fire(element, 'ajax:before')) {
94 | elCrossDomain = element.data('cross-domain');
95 | crossDomain = elCrossDomain === undefined ? null : elCrossDomain;
96 | withCredentials = element.data('with-credentials') || null;
97 | dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType);
98 |
99 | if (element.is('form')) {
100 | method = element.attr('method');
101 | url = element.attr('action');
102 | data = element.serializeArray();
103 | // memoized value from clicked submit button
104 | var button = element.data('ujs:submit-button');
105 | if (button) {
106 | data.push(button);
107 | element.data('ujs:submit-button', null);
108 | }
109 | } else if (element.is(rails.inputChangeSelector)) {
110 | method = element.data('method');
111 | url = element.data('url');
112 | data = element.serialize();
113 | if (element.data('params')) data = data + "&" + element.data('params');
114 | } else if (element.is(rails.buttonClickSelector)) {
115 | method = element.data('method') || 'get';
116 | url = element.data('url');
117 | data = element.serialize();
118 | if (element.data('params')) data = data + "&" + element.data('params');
119 | } else {
120 | method = element.data('method');
121 | url = rails.href(element);
122 | data = element.data('params') || null;
123 | }
124 |
125 | options = {
126 | type: method || 'GET', data: data, dataType: dataType,
127 | // stopping the "ajax:beforeSend" event will cancel the ajax request
128 | beforeSend: function(xhr, settings) {
129 | if (settings.dataType === undefined) {
130 | xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script);
131 | }
132 | return rails.fire(element, 'ajax:beforeSend', [xhr, settings]);
133 | },
134 | success: function(data, status, xhr) {
135 | element.trigger('ajax:success', [data, status, xhr]);
136 | },
137 | complete: function(xhr, status) {
138 | element.trigger('ajax:complete', [xhr, status]);
139 | },
140 | error: function(xhr, status, error) {
141 | element.trigger('ajax:error', [xhr, status, error]);
142 | },
143 | crossDomain: crossDomain
144 | };
145 |
146 | // There is no withCredentials for IE6-8 when
147 | // "Enable native XMLHTTP support" is disabled
148 | if (withCredentials) {
149 | options.xhrFields = {
150 | withCredentials: withCredentials
151 | };
152 | }
153 |
154 | // Only pass url to `ajax` options if not blank
155 | if (url) { options.url = url; }
156 |
157 | var jqxhr = rails.ajax(options);
158 | element.trigger('ajax:send', jqxhr);
159 | return jqxhr;
160 | } else {
161 | return false;
162 | }
163 | },
164 |
165 | // Handles "data-method" on links such as:
166 | // Delete
167 | handleMethod: function(link) {
168 | var href = rails.href(link),
169 | method = link.data('method'),
170 | target = link.attr('target'),
171 | csrfToken = $('meta[name=csrf-token]').attr('content'),
172 | csrfParam = $('meta[name=csrf-param]').attr('content'),
173 | form = $(''),
174 | metadataInput = ' ';
175 |
176 | if (csrfParam !== undefined && csrfToken !== undefined) {
177 | metadataInput += ' ';
178 | }
179 |
180 | if (target) { form.attr('target', target); }
181 |
182 | form.hide().append(metadataInput).appendTo('body');
183 | form.submit();
184 | },
185 |
186 | /* Disables form elements:
187 | - Caches element value in 'ujs:enable-with' data store
188 | - Replaces element text with value of 'data-disable-with' attribute
189 | - Sets disabled property to true
190 | */
191 | disableFormElements: function(form) {
192 | form.find(rails.disableSelector).each(function() {
193 | var element = $(this), method = element.is('button') ? 'html' : 'val';
194 | element.data('ujs:enable-with', element[method]());
195 | element[method](element.data('disable-with'));
196 | element.prop('disabled', true);
197 | });
198 | },
199 |
200 | /* Re-enables disabled form elements:
201 | - Replaces element text with cached value from 'ujs:enable-with' data store (created in `disableFormElements`)
202 | - Sets disabled property to false
203 | */
204 | enableFormElements: function(form) {
205 | form.find(rails.enableSelector).each(function() {
206 | var element = $(this), method = element.is('button') ? 'html' : 'val';
207 | if (element.data('ujs:enable-with')) element[method](element.data('ujs:enable-with'));
208 | element.prop('disabled', false);
209 | });
210 | },
211 |
212 | /* For 'data-confirm' attribute:
213 | - Fires `confirm` event
214 | - Shows the confirmation dialog
215 | - Fires the `confirm:complete` event
216 |
217 | Returns `true` if no function stops the chain and user chose yes; `false` otherwise.
218 | Attaching a handler to the element's `confirm` event that returns a `falsy` value cancels the confirmation dialog.
219 | Attaching a handler to the element's `confirm:complete` event that returns a `falsy` value makes this function
220 | return false. The `confirm:complete` event is fired whether or not the user answered true or false to the dialog.
221 | */
222 | allowAction: function(element) {
223 | var message = element.data('confirm'),
224 | answer = false, callback;
225 | if (!message) { return true; }
226 |
227 | if (rails.fire(element, 'confirm')) {
228 | answer = rails.confirm(message);
229 | callback = rails.fire(element, 'confirm:complete', [answer]);
230 | }
231 | return answer && callback;
232 | },
233 |
234 | // Helper function which checks for blank inputs in a form that match the specified CSS selector
235 | blankInputs: function(form, specifiedSelector, nonBlank) {
236 | var inputs = $(), input, valueToCheck,
237 | selector = specifiedSelector || 'input,textarea',
238 | allInputs = form.find(selector);
239 |
240 | allInputs.each(function() {
241 | input = $(this);
242 | valueToCheck = input.is('input[type=checkbox],input[type=radio]') ? input.is(':checked') : input.val();
243 | // If nonBlank and valueToCheck are both truthy, or nonBlank and valueToCheck are both falsey
244 | if (!valueToCheck === !nonBlank) {
245 |
246 | // Don't count unchecked required radio if other radio with same name is checked
247 | if (input.is('input[type=radio]') && allInputs.filter('input[type=radio]:checked[name="' + input.attr('name') + '"]').length) {
248 | return true; // Skip to next input
249 | }
250 |
251 | inputs = inputs.add(input);
252 | }
253 | });
254 | return inputs.length ? inputs : false;
255 | },
256 |
257 | // Helper function which checks for non-blank inputs in a form that match the specified CSS selector
258 | nonBlankInputs: function(form, specifiedSelector) {
259 | return rails.blankInputs(form, specifiedSelector, true); // true specifies nonBlank
260 | },
261 |
262 | // Helper function, needed to provide consistent behavior in IE
263 | stopEverything: function(e) {
264 | $(e.target).trigger('ujs:everythingStopped');
265 | e.stopImmediatePropagation();
266 | return false;
267 | },
268 |
269 | // replace element's html with the 'data-disable-with' after storing original html
270 | // and prevent clicking on it
271 | disableElement: function(element) {
272 | element.data('ujs:enable-with', element.html()); // store enabled state
273 | element.html(element.data('disable-with')); // set to disabled state
274 | element.bind('click.railsDisable', function(e) { // prevent further clicking
275 | return rails.stopEverything(e);
276 | });
277 | },
278 |
279 | // restore element to its original state which was disabled by 'disableElement' above
280 | enableElement: function(element) {
281 | if (element.data('ujs:enable-with') !== undefined) {
282 | element.html(element.data('ujs:enable-with')); // set to old enabled state
283 | element.removeData('ujs:enable-with'); // clean up cache
284 | }
285 | element.unbind('click.railsDisable'); // enable element
286 | }
287 |
288 | };
289 |
290 | if (rails.fire($document, 'rails:attachBindings')) {
291 |
292 | $.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { rails.CSRFProtection(xhr); }});
293 |
294 | $document.delegate(rails.linkDisableSelector, 'ajax:complete', function() {
295 | rails.enableElement($(this));
296 | });
297 |
298 | $document.delegate(rails.linkClickSelector, 'click.rails', function(e) {
299 | var link = $(this), method = link.data('method'), data = link.data('params'), metaClick = e.metaKey || e.ctrlKey;
300 | if (!rails.allowAction(link)) return rails.stopEverything(e);
301 |
302 | if (!metaClick && link.is(rails.linkDisableSelector)) rails.disableElement(link);
303 |
304 | if (link.data('remote') !== undefined) {
305 | if (metaClick && (!method || method === 'GET') && !data) { return true; }
306 |
307 | var handleRemote = rails.handleRemote(link);
308 | // response from rails.handleRemote() will either be false or a deferred object promise.
309 | if (handleRemote === false) {
310 | rails.enableElement(link);
311 | } else {
312 | handleRemote.error( function() { rails.enableElement(link); } );
313 | }
314 | return false;
315 |
316 | } else if (link.data('method')) {
317 | rails.handleMethod(link);
318 | return false;
319 | }
320 | });
321 |
322 | $document.delegate(rails.buttonClickSelector, 'click.rails', function(e) {
323 | var button = $(this);
324 | if (!rails.allowAction(button)) return rails.stopEverything(e);
325 |
326 | rails.handleRemote(button);
327 | return false;
328 | });
329 |
330 | $document.delegate(rails.inputChangeSelector, 'change.rails', function(e) {
331 | var link = $(this);
332 | if (!rails.allowAction(link)) return rails.stopEverything(e);
333 |
334 | rails.handleRemote(link);
335 | return false;
336 | });
337 |
338 | $document.delegate(rails.formSubmitSelector, 'submit.rails', function(e) {
339 | var form = $(this),
340 | remote = form.data('remote') !== undefined,
341 | blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector),
342 | nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector);
343 |
344 | if (!rails.allowAction(form)) return rails.stopEverything(e);
345 |
346 | // skip other logic when required values are missing or file upload is present
347 | if (blankRequiredInputs && form.attr("novalidate") == undefined && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) {
348 | return rails.stopEverything(e);
349 | }
350 |
351 | if (remote) {
352 | if (nonBlankFileInputs) {
353 | // slight timeout so that the submit button gets properly serialized
354 | // (make it easy for event handler to serialize form without disabled values)
355 | setTimeout(function(){ rails.disableFormElements(form); }, 13);
356 | var aborted = rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]);
357 |
358 | // re-enable form elements if event bindings return false (canceling normal form submission)
359 | if (!aborted) { setTimeout(function(){ rails.enableFormElements(form); }, 13); }
360 |
361 | return aborted;
362 | }
363 |
364 | rails.handleRemote(form);
365 | return false;
366 |
367 | } else {
368 | // slight timeout so that the submit button gets properly serialized
369 | setTimeout(function(){ rails.disableFormElements(form); }, 13);
370 | }
371 | });
372 |
373 | $document.delegate(rails.formInputClickSelector, 'click.rails', function(event) {
374 | var button = $(this);
375 |
376 | if (!rails.allowAction(button)) return rails.stopEverything(event);
377 |
378 | // register the pressed submit button
379 | var name = button.attr('name'),
380 | data = name ? {name:name, value:button.val()} : null;
381 |
382 | button.closest('form').data('ujs:submit-button', data);
383 | });
384 |
385 | $document.delegate(rails.formSubmitSelector, 'ajax:beforeSend.rails', function(event) {
386 | if (this == event.target) rails.disableFormElements($(this));
387 | });
388 |
389 | $document.delegate(rails.formSubmitSelector, 'ajax:complete.rails', function(event) {
390 | if (this == event.target) rails.enableFormElements($(this));
391 | });
392 |
393 | $(function(){
394 | rails.refreshCSRFTokens();
395 | });
396 | }
397 |
398 | })( jQuery );
399 |
--------------------------------------------------------------------------------
/app/controllers/user_time_checks_controller.rb:
--------------------------------------------------------------------------------
1 | class UserTimeChecksController < ApplicationController
2 | unloadable
3 |
4 | # before_action :require_login, :authorize, :only => :index
5 | include SortHelper
6 | # before_action :authorize, :only => :index
7 | helper :sort
8 |
9 | def index
10 |
11 | time_checks= UserTimeCheck.
12 | select("#{UserTimeCheck.table_name}.*,sum(#{TimeEntry.table_name}.hours ) as logged_hours").
13 | joins("LEFT JOIN #{TimeEntry.table_name} on DATE(check_in_time) <= spent_on AND DATE(check_out_time) >= spent_on").
14 | group("#{UserTimeCheck.table_name}.id")
15 |
16 | @time_check_grid = initialize_grid(time_checks.where('check_in_time > ?', Time.now - 6.months),
17 | name: 'time_checks_grid',
18 | enable_export_to_csv: true,
19 | csv_field_separator: ';',
20 | csv_file_name: 'UserTimeChecks')#,
21 |
22 | export_grid_if_requested('time_checks_grid' => 'time_check_grid')
23 |
24 | end
25 | def all_trackers
26 | (Setting.plugin_redmine_leaves['tracker_names'] || '').split(',').delete_if { |index| index.blank? }
27 | end
28 |
29 | def user_time_activity_report
30 |
31 |
32 | @trackers=Tracker.all
33 |
34 | @time_checks_for_trackers = {}
35 | @users_with_logged_activities=User.
36 | select("Distinct #{User.table_name}.id as user_id,#{User.table_name}.firstname,#{User.table_name}.lastname")
37 | .joins("INNER JOIN #{TimeEntry.table_name} on #{User.table_name}.id= #{TimeEntry.table_name}.user_id")
38 | .where(" #{TimeEntry.table_name}.spent_on>=? and #{TimeEntry.table_name}.spent_on<=?",params[:date_from]||Date.today - 1.month,params[:date_to]||Date.today )
39 |
40 | @user_stats = {}
41 |
42 | @time_spent_on_tracker = {}
43 | @trackers.each do |tracker|
44 |
45 | @time_spent_on_tracker[tracker.name] = User.
46 | select("#{User.table_name}.lastname, #{User.table_name}.firstname, #{User.
47 | table_name}.id as user_id, #{Tracker.table_name}.id as tracker_id,#{Tracker.
48 | table_name}.name as tracker_name,count(#{Tracker.
49 | table_name}.name)as num_of_trackers,sum(#{TimeEntry.
50 | table_name}.hours ) as time_spent")
51 | .joins("INNER JOIN #{TimeEntry.table_name} on #{User.table_name}.id= #{TimeEntry.table_name}.user_id")
52 | .joins("INNER JOIN #{Issue.table_name} on #{Issue.table_name}.id= #{TimeEntry.table_name}.issue_id")
53 | .joins("INNER JOIN #{Tracker.table_name} on #{Issue.table_name}.tracker_id= #{Tracker.table_name}.id")
54 | .group("#{Tracker.table_name}.id, #{User.table_name}.id")
55 | .order("#{Tracker.table_name}.id")
56 | .where("#{Tracker.table_name}.name=? AND #{TimeEntry.
57 | table_name}.spent_on>=? AND #{TimeEntry.table_name}.spent_on<=?",
58 | tracker.name, params[:date_from]||Date.today - 1.month,params[:date_to]||Date.today )
59 | end
60 |
61 |
62 | # @trackers.each do |tracker|
63 | @all_trackers=all_trackers
64 |
65 | # Sum(CASE
66 | # WHEN CMTS_RQ.US_Pwr >=37 AND CMTS_RQ.US_Pwr <= 49
67 | # THEN 1
68 | # ELSE 0
69 | all_trackers.each do |tracker|
70 |
71 | user_tracker_stats = User.
72 | select("#{User.table_name}.lastname, #{User.table_name}.firstname, #{User.
73 | table_name}.id as user_id, #{Tracker.table_name}.id as tracker_id,#{Tracker.
74 | table_name}.name as tracker_name,
75 | sum(CASE
76 | WHEN #{Issue.table_name}.estimated_hours IS NOT NULL
77 | THEN estimated_hours
78 | ELSE 0
79 | end) as estimated_hours_on_tracker,
80 | count(#{Tracker.
81 | table_name}.name)as num_of_trackers,sum(#{TimeEntry.table_name}.hours ) as time_spent")
82 | .joins("INNER JOIN #{TimeEntry.table_name} on #{User.table_name}.id= #{TimeEntry.table_name}.user_id")
83 | .joins("INNER JOIN #{Issue.table_name} on #{Issue.table_name}.id= #{TimeEntry.table_name}.issue_id")
84 | .joins("INNER JOIN #{Tracker.table_name} on #{Issue.table_name}.tracker_id= #{Tracker.table_name}.id")
85 | .group("#{Tracker.table_name}.id, #{User.table_name}.id")
86 | .order("#{Tracker.table_name}.id")
87 | .where("#{Tracker.table_name}.name=? AND #{TimeEntry.
88 | table_name}.spent_on>=? AND #{TimeEntry.table_name}.spent_on<=?",
89 | tracker, params[:date_from]||Date.today - 1.month,params[:date_to]||Date.today )
90 |
91 | user_tracker_stats.each do |user_tracker_stat|
92 | @user_stats[user_tracker_stat.user_id] ||= {user: user_tracker_stat, trackers: {}}
93 | @user_stats[user_tracker_stat.user_id][:trackers][user_tracker_stat.tracker_name] = user_tracker_stat
94 | end
95 | end
96 |
97 | missed_due_dates=User.
98 | select("#{User.table_name}.firstname,#{User.table_name}.lastname,#{User.table_name}.id as user_id, count(#{Tracker.
99 | table_name}.id)as missed_dates")
100 | .joins("INNER JOIN #{TimeEntry.table_name} on #{User.table_name}.id= #{TimeEntry.table_name}.user_id")
101 | .joins("INNER JOIN #{Issue.table_name} on #{Issue.table_name}.id= #{TimeEntry.table_name}.issue_id")
102 | .joins("INNER JOIN #{Tracker.table_name} on #{Issue.table_name}.tracker_id= #{Tracker.table_name}.id")
103 | .group("#{User.table_name}.id,#{Issue.table_name}.id")
104 | .where(
105 | "#{Issue.table_name}.due_date< #{Issue.table_name}.closed_on
106 | and #{Issue.table_name}.due_date is not NULL
107 | and #{Issue.table_name }.due_date >=?
108 | and #{Issue.table_name }.due_date <=?",params[:date_from]||Date.today - 1.month,params[:date_to]||Date.today )
109 |
110 | missed_due_dates.each do |missed_due_dates|
111 | @user_stats[missed_due_dates.user_id] ||= {user: missed_due_dates, trackers: {}}
112 |
113 | @user_stats[missed_due_dates.user_id][:missed_due_dates] = missed_due_dates
114 | end
115 | end
116 |
117 |
118 |
119 |
120 | def user_time_activity_report_monthly
121 |
122 | @trackers=Tracker.all
123 | @months_and_years=User.
124 | select("Distinct #{User.table_name}.id as user_id,month(#{TimeEntry.table_name}.spent_on) as month,year(#{TimeEntry.table_name}.spent_on) as year")
125 | .joins("INNER JOIN #{TimeEntry.table_name} on #{User.table_name}.id= #{TimeEntry.table_name}.user_id")
126 | .where(" #{TimeEntry.table_name}.spent_on>=? and #{TimeEntry.table_name}.spent_on<=?",params[:date_from]||Date.today - 1.month,params[:date_to]||Date.today )
127 | .order("user_id")
128 | count_users=0
129 | @months_and_years.each do |user|
130 | unless user.nil?
131 | count_users=count_users+1
132 | end
133 | end
134 |
135 | @time_spent_on_tracker = {}
136 | @missed_dates = {}
137 | @all_trackers=all_trackers
138 |
139 |
140 | @months_and_years.each do |user|
141 | # @trackers.each do |tracker|
142 | all_trackers.each do |tracker|
143 | @time_spent_on_tracker[tracker+user.user_id.to_s+user.month.to_s+user.year.to_s] = User.
144 | select(" #{User.table_name}.firstname,year(#{TimeEntry.table_name}.spent_on)as year,
145 | month(#{TimeEntry.table_name}.spent_on) as month")
146 | .joins("INNER JOIN #{TimeEntry.table_name} on #{User.table_name}.id= #{TimeEntry.table_name}.user_id")
147 | .joins("INNER JOIN #{Issue.table_name} on #{Issue.table_name}.id= #{TimeEntry.table_name}.issue_id")
148 | .joins("INNER JOIN #{Tracker.table_name} on #{Issue.table_name}.tracker_id= #{Tracker.table_name}.id")
149 | .group("#{Tracker.table_name}.id,#{User.table_name}.id,year(#{TimeEntry.table_name}.spent_on),month(#{TimeEntry.table_name}.spent_on)")
150 | .select("#{Tracker.table_name}.id as tracker_id,#{Tracker.table_name}.name as tracker_name,
151 | count(#{Tracker.table_name}.name)as num_of_trackers,
152 | sum(CASE
153 | WHEN #{Issue.table_name}.estimated_hours IS NOT NULL
154 | THEN estimated_hours
155 | ELSE 0
156 | end) as estimated_hours_on_tracker,
157 | sum(#{TimeEntry.table_name}.hours ) as time_spent")
158 | .order("year(#{TimeEntry.table_name}.spent_on),month(#{TimeEntry.table_name}.spent_on),#{Tracker.table_name}.id")
159 | .where("#{Tracker.table_name}.name=?
160 | and #{TimeEntry.table_name}.spent_on>=?
161 | and #{TimeEntry.table_name}.spent_on<=?
162 | and month(#{TimeEntry.table_name}.spent_on)=?
163 | and year(#{TimeEntry.table_name}.spent_on)=?
164 | and #{User.table_name}.id=?",tracker,params[:date_from]||Date.today - 1.month,params[:date_to]||Date.today ,user.month,user.year,user.user_id)
165 |
166 |
167 | end
168 |
169 | @missed_dates[user.user_id.to_s+user.month.to_s+user.year.to_s]=User.
170 | select(" #{User.table_name}.firstname,#{User.table_name}.lastname,
171 | year(#{TimeEntry.table_name}.spent_on)as year,
172 | month(#{TimeEntry.table_name}.spent_on) as month,
173 | count(#{Tracker.table_name}.id)as missed_dates")
174 | .joins("INNER JOIN #{TimeEntry.table_name} on #{User.table_name}.id= #{TimeEntry.table_name}.user_id")
175 | .joins("INNER JOIN #{Issue.table_name} on #{Issue.table_name}.id= #{TimeEntry.table_name}.issue_id")
176 | .joins("INNER JOIN #{Tracker.table_name} on #{Issue.table_name}.tracker_id= #{Tracker.table_name}.id")
177 | .group("#{User.table_name}.id,
178 | #{Issue.table_name}.id,
179 | year(#{TimeEntry.table_name}.spent_on),
180 | month(#{TimeEntry.table_name}.spent_on)")
181 | .where("#{Issue.table_name}.due_date< #{Issue.table_name}.closed_on
182 | and #{Issue.table_name}.due_date is not NULL
183 | and #{TimeEntry.table_name}.spent_on>=?
184 | and #{TimeEntry.table_name}.spent_on<=?
185 | and month(#{TimeEntry.table_name}.spent_on)=?
186 | and year(#{TimeEntry.table_name}.spent_on)=?
187 | and #{User.table_name}.id=?",params[:date_from]||Date.today - 1.month,params[:date_to]||Date.today ,user.month,user.year,user.user_id)
188 | .order("year(#{Issue.table_name}.start_date),month(#{Issue.table_name}.start_date)")
189 |
190 | end
191 |
192 | @trackers=Tracker.all
193 |
194 |
195 |
196 |
197 | end
198 |
199 |
200 |
201 |
202 | def user_time_reporting
203 |
204 | time_checks = UserTimeCheck.select("user_id, check_in_time,check_out_time,
205 | AVG(check_in_time) as avg_check_in_time,
206 | AVG(check_out_time) as avg_check_out_time,
207 | avg(time_spent) as average_time")
208 | .includes(:user)
209 | .group('user_id')
210 | .where("check_out_time IS NOT NULL")#
211 |
212 | @time_report_grid = initialize_grid(time_checks.where('check_in_time > ?', Time.now - 6.months),
213 | name: 'time_checks_grid',
214 | enable_export_to_csv: true,
215 | csv_field_separator: ';',
216 | csv_file_name: 'UserTimeCustom')#,
217 |
218 | export_grid_if_requested('time_checks_grid' => 'time_report_grid')
219 |
220 |
221 | end
222 |
223 |
224 |
225 |
226 | def user_time_reporting_weekly
227 |
228 | time_checks = UserTimeCheck.select("check_in_time as weekdays,week(check_in_time) as week,year(check_in_time) as year,check_in_time,
229 | check_out_time ,user_id,
230 | AVG(check_in_time) as avg_check_in_time,
231 | AVG(check_out_time) as avg_check_out_time,
232 | sum(time_spent) as time_spent,avg(time_spent) as average_time").
233 | includes(:user).
234 | group('user_id,year(check_in_time),week(check_in_time)').
235 | order('year(check_in_time),week(check_in_time)')#.includes(:user)
236 |
237 | @time_report_grid_weekly = initialize_grid(time_checks.where('check_in_time > ?', Time.now - 6.months),
238 | name: 'time_checks_grid',
239 | enable_export_to_csv: true,
240 | csv_field_separator: ';',
241 | csv_file_name: 'UserTimeWeekly')#,
242 |
243 | export_grid_if_requested('time_checks_grid' => 'time_report_grid_weekly')
244 |
245 |
246 |
247 |
248 | end
249 |
250 |
251 |
252 | def user_time_reporting_monthly
253 |
254 |
255 | time_checks = UserTimeCheck.includes(:user)
256 | .select("check_in_time, check_out_time, user_id,
257 | AVG(check_in_time) as avg_check_in_time,
258 | AVG(check_out_time) as avg_check_out_time,
259 | sum(time_spent) as time_spent,avg(time_spent) as average_time")
260 | .group('user_id,year(check_in_time),month(check_in_time)')
261 | .order('year(check_in_time),month(check_in_time),user_id')
262 | .where("check_out_time IS NOT NULL")
263 | @time_report_grid_monthly = initialize_grid(time_checks,
264 | name: 'time_checks_grid',
265 | enable_export_to_csv: true,
266 | # conditions: ["check_in_time > ?", Time.now - 12.months],
267 | csv_field_separator: ';',
268 | csv_file_name: 'UserTimeMonthly')#,
269 |
270 | export_grid_if_requested('time_checks_grid' => 'time_report_grid_monthly')
271 |
272 | end
273 |
274 |
275 |
276 |
277 |
278 | def edit
279 | @time_checks = UserTimeCheck.find(params[:id])
280 | end
281 |
282 | def update
283 | @time_checks = UserTimeCheck.find(params[:id])
284 | if @time_checks.update_attributes(params[:user_time_check])
285 | redirect_to user_time_checks_path,
286 | notice: "User Time Check for #{@time_checks.user.name}
287 | on #{@time_checks.check_in_time.to_date} Updated.
288 | #{view_context.link_to t(:link_edit), edit_user_time_check_path(@time_checks)}".html_safe
289 | else
290 | redirect_to edit_user_time_check_path(@time_checks), :flash => { :error => "Invalid Input!" }
291 | end
292 | end
293 |
294 | def import
295 | begin
296 | UserTimeCheck.import(params[:file])
297 | redirect_to user_time_checks_path, notice: t(:notice_time_check_file_imported) if params[:file]
298 | rescue StandardError => e
299 | puts "#{e.message}\n#{e.backtrace.join("\n")}"
300 | redirect_to user_time_checks_path, flash: { error: t(:error_invalid_file_format) }
301 | end
302 | end
303 |
304 | def check_in
305 | checkin_timechecks = UserTimeCheck.where(['user_id = ? AND check_out_time IS NULL', User.current.id])
306 |
307 | if checkin_timechecks.empty?
308 | @user_time_check = UserTimeCheck.create(user_id: User.current.id, check_in_time: DateTime.now)
309 | else
310 | flash.now[:error] = t(:error_checkout_first)
311 | @user_time_check = checkin_timechecks.first
312 | end
313 | end
314 |
315 | def check_out
316 | checkout_timechecks = UserTimeCheck.where(['user_id = ? AND check_out_time IS NULL', User.current.id])
317 |
318 | if checkout_timechecks.empty?
319 | flash.now[:error] = t(:error_checkin_first)
320 | @user_time_check = UserTimeCheck.new(:user_id => User.current.id)
321 |
322 | else
323 | @user_time_check = checkout_timechecks.first
324 | @user_time_check.update_attributes(check_out_time: DateTime.now)
325 |
326 |
327 | @time_entries= TimeEntry.where(user_id: User.current.id , created_on: (@user_time_check.check_in_time)..@user_time_check.check_out_time, spent_on: [@user_time_check.check_in_time.to_date,@user_time_check.check_out_time.to_date])
328 |
329 |
330 | logged_in_time= @time_entries.sum(:hours)
331 | checked_time = @user_time_check.check_out_time - @user_time_check.check_in_time
332 |
333 | if logged_in_time<0.9*(checked_time/3600)
334 | flash.now[:error] = t(:error_less_time_logged)
335 | #@assigned_issues= Issue.where(assigned_to_id: User.current.id)
336 | @assigned_issues= Issue.where(assigned_to_id: User.current.id).joins(:status).
337 | where("#{IssueStatus.table_name}.is_closed" => false)
338 |
339 | #@new_time_entries = Array.new(3) { assigned_issue.time_entries.build }
340 | @new_time_entries = []
341 | @assigned_issues.each do |assigned_issue|
342 | #@new_time_entries << TimeEntry.new(:issue_id => assigned_issue.id)
343 | @new_time_entries << assigned_issue.time_entries.build
344 | end
345 | end
346 | end
347 | end
348 |
349 | def create_time_entries
350 | logger.debug "#{'*'*80}\nReceived parameters: #{params.inspect}\n#{'*'*80}"
351 | #"time_entries"=>{"issue_id"=>["1", "2"], "hours"=>["1", "2"], "activity_id"=>["8", "8"], "comments"=>["asim", "hello"]}
352 | @new_time_entries = []
353 | # issue_ids = params[:issue_id]
354 | # issue_ids.each_index do |idx|
355 | # time_entries << TimeEntry.create(:issue_id => issue_ids[idx], :hours => params[:hours][idx])
356 | # end
357 |
358 | time_entry_paramss = params[:time_entries] || []
359 | time_entry_paramss.each do |time_entry_params|
360 | time_entry_this = TimeEntry.new(time_entry_params) # This solves the .permit problem : See Model
361 | time_entry_this.user_id = User.current.id
362 | time_entry_this.save
363 | @new_time_entries << time_entry_this
364 | end
365 | #@assigned_issues= Issue.where(assigned_to_id: User.current.id)
366 | @assigned_issues= Issue.where(assigned_to_id: User.current.id).joins(:status).
367 | where("#{IssueStatus.table_name}.is_closed" => false)
368 | @user_time_check = UserTimeCheck.where(["user_id = ? and check_out_time IS NOT NULL", User.current.id]).limit(1).order('id DESC').first
369 | # @time_entries= TimeEntry.where(user_id: User.current.id , spent_on: (@user_time_check.check_in_time)..@user_time_check.check_out_time)
370 | @time_entries= TimeEntry.where(user_id: User.current.id , created_on: (@user_time_check.check_in_time)..@user_time_check.check_out_time+1.hour, spent_on: [@user_time_check.check_in_time.to_date,@user_time_check.check_out_time.to_date])
371 |
372 | logged_time= @time_entries.sum(:hours)
373 | checked_time = @user_time_check.check_out_time - @user_time_check.check_in_time
374 |
375 | if logged_time<0.90*(checked_time/3600) #may changed this
376 | render 'check_out'
377 | else
378 | render 'checkout_timelog_success'
379 | end
380 |
381 | end
382 |
383 |
384 | end
385 |
--------------------------------------------------------------------------------
/app/models/user_time_check.rb:
--------------------------------------------------------------------------------
1 | class UserTimeCheck < ActiveRecord::Base
2 | unloadable
3 |
4 | extend Redmine::Utils::DateCalculation
5 |
6 | belongs_to :user
7 |
8 | validates :time_spent, :check_in_time, :check_out_time, presence: true, on: :update
9 | validate :correctness_of_user_time_checks
10 |
11 | class << self
12 | def checked_in?(user_id)
13 | exists?(['user_id = ? and check_out_time IS NULL', user_id])
14 | end
15 | end
16 |
17 | INDEX_ZK_NAME = 4
18 | INDEX_ZK_DATETIME = 1
19 |
20 |
21 | def correctness_of_user_time_checks
22 | unless check_in_time.nil? || check_out_time.nil?
23 | if check_in_time > check_out_time
24 | errors.add(:check_in_time, ": cannot be greater than Check-Out Time")
25 | end
26 | end
27 | end
28 |
29 | class << self
30 | def time_loggers_group
31 | Group.find(Setting.plugin_redmine_leaves['time_loggers_group'])
32 | end
33 | def time_log_receivers_group
34 | Group.find(Setting.plugin_redmine_leaves['time_log_receivers_group'])
35 | end
36 |
37 | def num_min_working_hours
38 | Setting.plugin_redmine_leaves['num_min_working_hours'].to_i || 8
39 | end
40 |
41 | def default_leave_type
42 | Setting.plugin_redmine_leaves['default_type'] || 'Annual'
43 | end
44 |
45 | def email_reminder_for_issue_time_log
46 | cc_group = []
47 | reminder_time = ''
48 |
49 | if args.arg1 == 'daily'
50 | cc_group = Setting.plugin_redmine_leaves['daily_reminder']
51 | reminder_time = 'Daily'
52 | elsif args.arg1 == 'weekly'
53 | cc_group = Setting.plugin_redmine_leaves['weekly_reminder']
54 | reminder_time = 'Weekly'
55 | elsif args.arg1 == 'monthly'
56 | cc_group = Setting.plugin_redmine_leaves['monthly_reminder']
57 | reminder_time = 'Monthly'
58 | end
59 |
60 | all_users = User.active
61 | all_users.find_each do |user|
62 | assigned_issues= Issue.where(assigned_to_id: user.id).joins(:status).
63 | where("#{IssueStatus.table_name}.is_closed" => false)
64 | user_leave = UserLeave.where(user_id: user.id, leave_date: Date.today)
65 | assigned_issues.each do |issue|
66 | time_entry = TimeEntry.find_by_issue_id(issue)
67 | if !time_entry
68 | #if user never logged time for this issue
69 | LogTimeReminderMailer.reminder_email(user, issue, cc_group, reminder_time).deliver_now
70 | elsif !(time_entry.updated_on.to_date == Date.today) && !user_leave.exists?
71 | #if user is present and did not log time today
72 | LogTimeReminderMailer.reminder_email(user, issue, cc_group, reminder_time).deliver_now
73 | end
74 | end
75 | end
76 | end
77 |
78 | def report_activity(report_days = 1, report_projects = true,
79 | notify_missing_time = true, mark_leave_after_days = -1, error_tolerance = 0.25)
80 | projects = Project.active.includes(members: :user) #get project timesheet
81 |
82 | start_date = Date.today - report_days.day
83 | end_date = Date.today - 1.day
84 | end_date = start_date if start_date > end_date
85 |
86 | return if working_days(start_date, end_date + 1.day) < 1
87 |
88 | users = User.in_group(time_loggers_group).where(status: User::STATUS_ACTIVE)
89 |
90 |
91 | email_group_timesheet(time_log_receivers_group, time_loggers_group, start_date, end_date) if report_projects
92 |
93 | email_project_timesheets(projects, start_date, end_date) if report_projects
94 |
95 | email_users_missing_hours(users, start_date, end_date) if notify_missing_time
96 |
97 | mark_leave_for_missing_hours(users, start_date, end_date, mark_leave_after_days, error_tolerance) if mark_leave_after_days > 0
98 | end
99 |
100 | def mark_leave_for_missing_hours(users, start_date, end_date, mark_leave_after_days, error_tolerance = 0.25)
101 | total_days = working_days(start_date, end_date + 1.day)
102 | num_working_hours = total_days * num_min_working_hours
103 |
104 | start_date = start_date - mark_leave_after_days.days
105 | end_date = end_date - mark_leave_after_days.days
106 |
107 | user_ids = users.map(&:id)
108 | (start_date..end_date).each do |curr_date|
109 | num_working_hours = num_min_working_hours * (1 - error_tolerance)
110 |
111 | logged_time_user_ids = User.joins(:time_entries).where("#{TimeEntry.table_name}.spent_on" => curr_date,
112 | "#{User.table_name}.id" => user_ids).ids
113 |
114 | missing_hours_users = TimeEntry.where(spent_on: curr_date,
115 | user_id: logged_time_user_ids).group(:user_id).
116 | having("sum_hours < #{num_working_hours}").sum('hours')
117 |
118 | users_missing_time = User.where(id: missing_hours_users.keys)
119 |
120 | users_missing_time.each do |user|
121 | leave_day = calculate_leave_days(user, curr_date, curr_date)
122 |
123 | unless leave_day > 0
124 | leave_weight = (num_min_working_hours - missing_hours_user[user.id].to_f) / num_min_working_hours
125 | UserLeave.create!(user: user, leave_type: default_leave_type, comments: 'Missing time log', fractional_leave: leave_weight)
126 | end
127 | end
128 |
129 | no_hours_user_ids = user_ids - logged_time_user_ids
130 | users_no_time = User.where(id: no_hours_user_ids)
131 | users_no_time.each do |user|
132 | leave_days = calculate_leave_days(user, curr_date, curr_date)
133 | unless leave_days > 0
134 | leave_weight = 1
135 | UserLeave.create!(user: user, leave_type: default_leave_type, comments: 'Missing time log', fractional_leave: leave_weight)
136 | end
137 | end
138 | end
139 | end
140 |
141 | def calculate_leave_days(user, start_date, end_date)
142 | leaves = user.user_leaves.where(leave_date: start_date..end_date)
143 | leaves.map do |leave|
144 | leave.fractional_leave > 1 ? 1 : leave.fractional_leave
145 | end.reduce(:+).to_f
146 | end
147 | private :calculate_leave_days
148 |
149 |
150 | def email_users_missing_hours(users, start_date, end_date)
151 | total_days = working_days(start_date, end_date + 1.day)
152 | num_working_hours = total_days * num_min_working_hours
153 | user_ids = users.map(&:id)
154 |
155 | logged_time_user_ids = User.joins(:time_entries).where("#{TimeEntry.table_name}.spent_on" => start_date..end_date,
156 | "#{User.table_name}.id" => user_ids).ids
157 |
158 | missing_hours_users = TimeEntry.where(spent_on: start_date..end_date,
159 | user_id: logged_time_user_ids).group(:user_id).
160 | having("sum(hours) < ?", num_working_hours - 0.001).sum("hours")
161 | users_missing_time = User.where(id: missing_hours_users.keys)
162 |
163 | users_missing_time.each do |user|
164 | leave_days = calculate_leave_days(user, start_date, end_date)
165 |
166 | if leave_days > 0
167 | #recalculate missing hours for user
168 | num_working_hours = (total_days - leave_days) * num_min_working_hours
169 |
170 | #take into account leave time for user
171 | missing_hours_user = TimeEntry.where(spent_on: start_date..end_date,
172 | user_id: user.id).group(:user_id).
173 | having("sum(hours) < ?", num_working_hours - 0.001).sum('hours')
174 |
175 | LeaveMailer.missing_time_log(user, start_date, end_date,
176 | missing_hours_user[user.id]).deliver if missing_hours_user[user.id]
177 | else
178 | LeaveMailer.missing_time_log(user, start_date, end_date,
179 | missing_hours_users[user.id]).deliver
180 | end
181 | end
182 |
183 | no_hours_user_ids = user_ids - logged_time_user_ids
184 | users_no_time = User.where(id: no_hours_user_ids)
185 | users_no_time.each do |user|
186 | leave_days = calculate_leave_days(user, start_date, end_date)
187 | if leave_days > 0
188 | #recalculate missing hours for user
189 | num_working_hours = (total_days - leave_days) * num_min_working_hours
190 |
191 | if num_working_hours > 0
192 | LeaveMailer.missing_time_log(user, start_date, end_date, 0).deliver
193 | end
194 | else
195 | LeaveMailer.missing_time_log(user, start_date, end_date, 0).deliver
196 | end
197 | end
198 | end
199 |
200 | def email_project_timesheets(projects, start_date, end_date)
201 | User.current.admin = true #set admin = true so that all time entries can be fetched
202 | for project in projects #get project Members for every project and send it to send_time_sheet_emial
203 | project_members = project.members
204 | for project_member in project_members
205 | email_project_timesheet(project_member.user, project_member.project,
206 | start_date, end_date) if project_member.roles.
207 | detect{|role|role.allowed_to?(:receive_timesheet_email)}
208 | end
209 | end
210 | end
211 |
212 | def email_project_timesheet(user, project, start_date, end_date)
213 | total_days = working_days(start_date, end_date + 1.day)
214 | return if total_days < 1
215 | #Make new object for time sheet get user ids form project and assign it to users
216 |
217 | billable_users_for_project = billable_users(project)
218 | time_entries_billable = TimeEntry.where(user_id: billable_users_for_project,
219 | spent_on: start_date..end_date).
220 | includes(:activity, :project, :user)
221 | time_entries_users = {}
222 | time_entries_billable.each do |time_entry|
223 | time_entries_users[time_entry.user] ||= {time_entries: [], user_leaves: []}
224 | time_entries_users[time_entry.user][:time_entries] << time_entry
225 | end
226 |
227 | if time_entries_users.size < billable_users_for_project.size
228 | @@default_activity ||= TimeEntryActivity.first
229 | #put empty time entries for users who have not logged their time
230 | billable_users_for_project.each do |billable_user|
231 | time_entries_users[billable_user] ||= {
232 | time_entries: [build_empty_time_entry(billable_user, project, end_date)],
233 | user_leaves: []
234 | }
235 | end
236 | end
237 |
238 | time_entries_other = TimeEntry.where("user_id NOT IN (?) and project_id = ? and spent_on >= ? and spent_on <= ?",
239 | billable_users_for_project, project, start_date, end_date).
240 | includes(:activity, :project, :user)
241 | time_entries_other.each do |time_entry|
242 | time_entries_users[time_entry.user] ||= {time_entries: [], user_leaves: []}
243 | time_entries_users[time_entry.user][:time_entries] << time_entry
244 | end
245 |
246 | user_leaves = UserLeave.where(leave_date: start_date..end_date).includes(:user)
247 | user_leaves.each do |user_leave|
248 | time_entries_users[user_leave.user] ||= {time_entries: [], user_leaves: []}
249 | time_entries_users[user_leave.user][:user_leaves] << user_leave
250 |
251 | end
252 |
253 | timesheet_table = fetch_timesheet_table(time_entries_users)
254 | LeaveMailer.project_timesheet([user], timesheet_table, project.name, start_date, end_date).deliver
255 | end
256 |
257 | def email_group_timesheet(receiver_group, logger_group, start_date, end_date)
258 | #ignore project if no time is logged whatsoever
259 | total_days = working_days(start_date, end_date + 1.day)
260 | return if total_days < 1
261 |
262 | receiver_users = User.in_group(receiver_group)
263 | users = User.in_group(logger_group)
264 | #Make new object for time sheet get user ids form project and assign it to users
265 |
266 | time_entries_billable = TimeEntry.where(user_id: users.map(&:id),
267 | spent_on: start_date..end_date).
268 | includes(:activity, :project, :user)
269 | time_entries_users = {}
270 | time_entries_billable.each do |time_entry|
271 | time_entries_users[time_entry.user] ||= {time_entries: [], user_leaves: []}
272 | time_entries_users[time_entry.user][:time_entries] << time_entry
273 | end
274 |
275 | if time_entries_users.size < users.size
276 | @@default_activity ||= TimeEntryActivity.first
277 | #put empty time entries for users who have not logged their time
278 | users.each do |billable_user|
279 | time_entries_users[billable_user] ||= {
280 | time_entries: [build_empty_time_entry(billable_user, nil, end_date)],
281 | user_leaves: []
282 | }
283 | end
284 | end
285 |
286 | time_entries_other = TimeEntry.where("user_id NOT IN (?) and spent_on >= ? and spent_on <= ?",
287 | users.map(&:id), start_date, end_date).
288 | includes(:activity, :project, :user)
289 | time_entries_other.each do |time_entry|
290 | time_entries_users[time_entry.user] ||= {time_entries: [], user_leaves: []}
291 | time_entries_users[time_entry.user][:time_entries] << time_entry
292 | end
293 |
294 | user_leaves = UserLeave.where(leave_date: start_date..end_date).includes(:user)
295 | user_leaves.each do |user_leave|
296 | time_entries_users[user_leave.user] ||= {time_entries: [], user_leaves: []}
297 | time_entries_users[user_leave.user][:user_leaves] << user_leave
298 |
299 | end
300 |
301 | timesheet_table = fetch_timesheet_table(time_entries_users)
302 | LeaveMailer.group_timesheet(receiver_users, timesheet_table, logger_group, start_date, end_date).deliver
303 | end
304 |
305 | def build_empty_time_entry(user_id, project, spent_on)
306 | @@default_activity ||= TimeEntryActivity.first
307 | time_entry = TimeEntry.new
308 | time_entry.user_id = user_id; time_entry.project = project
309 | time_entry.activity = @@default_activity; time_entry.hours = 0
310 | time_entry.spent_on = spent_on
311 | time_entry
312 | end
313 | private :build_empty_time_entry
314 |
315 | def billable_users(project)
316 | users = project.members.map(&:user)
317 | User.in_group(time_loggers_group).where(id: users.map(&:id))
318 | end
319 | private :billable_users
320 |
321 | def fetch_timesheet_table(time_entries_users)
322 | timesheet_table = []
323 | time_entries_users.each_pair do |user, time_leave_entries|
324 | time_entries = time_leave_entries[:time_entries]#: [], user_leaves: []}
325 | leaves = time_leave_entries[:user_leaves]#: [], user_leaves: []}
326 | project_total={} #hash to get the project total against particular project
327 | user_project_activity_hours = {} #hash to build a particular model Users=>Projects=>activities=>issues
328 | for time_entry in time_entries
329 | project_total[time_entry.project_id] ||= 0
330 | project_total[time_entry.project_id] += time_entry.hours
331 | user_project_activity_hours[user] ||= {}
332 | activity_hours = user_project_activity_hours[user][time_entry.project] ||= {}
333 |
334 | activity_hours[time_entry.activity.name] ||= [[],0]
335 | activity_hours[time_entry.activity.name][0] << time_entry.issue
336 | activity_hours[time_entry.activity.name][1] += time_entry.hours
337 | end
338 |
339 | user_project_activity_hours.each_pair do |user, project_activity_hours|
340 | user_total = 0 #total time spent by user in all projects
341 | project_activity_hours.each_pair do |project, activity_hours|
342 | project_total = 0 #total time spent on project
343 | activity_hours.each_pair do |activity_name, issue_hours|
344 | timesheet_table << {user: user, project: project, activity: activity_name,
345 | issues: issue_hours[0], hours: issue_hours[1], total_heading: nil, total: nil}
346 | project_total += issue_hours[1]
347 | end
348 | timesheet_table << {user: nil, project: nil, activity: nil,
349 | issues: nil, hours: nil, total_heading: 'Project Total', total: project_total}
350 | user_total += project_total
351 | end
352 | timesheet_table << {user: nil, project: nil, activity: nil,
353 | issues: nil, hours: nil, total_heading: 'Total Leaves',
354 | total: leaves.map(&:fractional_leave).reduce(&:+).to_f}
355 | timesheet_table << {user: nil, project: nil, activity: nil,
356 | issues: nil, hours: nil, total_heading: 'User Total ', total: user_total}
357 | end
358 | end
359 |
360 | timesheet_table
361 | end
362 | private :fetch_timesheet_table
363 |
364 | def as_csv
365 |
366 | CSV.generate do |csv|
367 | csv << ["USER ID", "User Name", "Check In Time", "Check Out Time ", "Time Spent", "Comments"] ## Header values of CSV
368 | all.each do |emp|
369 | csv << [emp.user_id, emp.user.name, emp.check_in_time, emp.check_in_time, emp.time_spent, emp.comments] ##Row values of CSV
370 | end
371 | end
372 | end
373 |
374 |
375 | def autogenerate_checkout(missed_checkout)
376 | if missed_checkout.check_in_time.hour < 16
377 | missed_checkout.update_attributes(check_out_time: missed_checkout.check_in_time + 6.hours,
378 | comments: t(:auto_generated_comment),time_spent: 6*60)
379 | else
380 | #consider previously marked check_in_time as actually check_out time
381 | missed_checkout.update_attributes(check_in_time: missed_checkout.check_in_time - 6.hours,
382 | check_out_time: missed_checkout.check_in_time,
383 | comments: t(:auto_generated_comment),time_spent: 6*60)
384 |
385 | end
386 | end
387 |
388 | def import(file, options = {:headers => false, :col_sep => "\t"} )
389 | unless file.nil?
390 | CSV.foreach(file.path, options) do |row|
391 | name = row[INDEX_ZK_NAME]
392 | date = row[INDEX_ZK_DATETIME]
393 | first_last_name = name.split(" ")
394 | user = User.where(firstname: first_last_name.first, lastname: first_last_name.last).first
395 | logger.debug "#{name}#{date.to_datetime}#{user}"
396 |
397 | unless name && date && user
398 | # missing name or date or no records found in database
399 | next
400 | end
401 |
402 | # date = "#{date} #{Time.zone}".to_datetime
403 | date=date.to_datetime
404 | missed_checkouts = UserTimeCheck.
405 | where(['user_id = ? AND check_in_time < ? AND check_out_time IS NULL',
406 | user.id, date.beginning_of_day])
407 | missed_checkouts.each do |missed_checkout|
408 | autogenerate_checkout(missed_checkout)
409 | end
410 |
411 | checkin_timechecks = UserTimeCheck.
412 | where(['user_id = ? AND check_in_time < ? AND check_in_time > ? AND check_out_time IS NULL',
413 | user.id, date.end_of_day, date.beginning_of_day])
414 | logger.debug checkin_timechecks
415 | if checkin_timechecks.empty?
416 | UserTimeCheck.create(user_id: user.id, check_in_time: date,check_out_time: nil)
417 | logger.debug "#{'*'*50}#{user}#{'*'*50}"
418 | else
419 | user_time_check = checkin_timechecks.first
420 | checked_time = date.to_time - user_time_check.check_in_time.to_time
421 | difference = Time.at(checked_time).utc.strftime("%H").to_i
422 | if user_time_check.check_in_time.to_date == date.to_date
423 | if difference > 1 && difference < 16
424 | #difference between first checkin and current pulled time is less than 16 and greater than 1 (meaning) not by mistake), so asssume checkout
425 | user_time_check.update_attributes(check_out_time: date,time_spent: checked_time/60)
426 | logger.debug '&'*50
427 | elsif difference > 16
428 | #difference between previous checkout and current is greater than 16, meaning that user missed out
429 | user_time_check.update_attributes(check_out_time: user_time_check.check_in_time + 6.hours,
430 | comments: t(:auto_generated_comment),time_spent: 6*60)
431 | # UserTimeCheck.create(user_id: user.id, check_in_time: date)
432 | logger.debug '$'*50
433 |
434 | else
435 | #ignore the check-in
436 | end
437 | elsif date.hour < 8 && difference > 1 && difference < 16
438 | #allow checkin/checkout for different days
439 | user_time_check.update_attributes(check_out_time: date,time_spent: checked_time/60)
440 | else
441 | #difference between previous checkout and current is greater than 16, meaning that user missed out
442 | autogenerate_checkout(user_time_check)
443 | UserTimeCheck.create(user_id: user.id, check_in_time: date)
444 | p '$'*50
445 | end
446 | end
447 | end
448 | end
449 | end
450 | end
451 | end
452 |
453 |
--------------------------------------------------------------------------------