tempest-time 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +133 -0
- data/LICENSE +21 -0
- data/README.md +8 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/tempest +18 -0
- data/lib/tempest_time.rb +4 -0
- data/lib/tempest_time/api/authorization.rb +17 -0
- data/lib/tempest_time/api/jira_api/authorization.rb +27 -0
- data/lib/tempest_time/api/jira_api/models/issue.rb +28 -0
- data/lib/tempest_time/api/jira_api/request.rb +40 -0
- data/lib/tempest_time/api/jira_api/requests/get_issue.rb +29 -0
- data/lib/tempest_time/api/jira_api/requests/get_user_issues.rb +36 -0
- data/lib/tempest_time/api/jira_api/response.rb +5 -0
- data/lib/tempest_time/api/jira_api/responses/get_issue.rb +16 -0
- data/lib/tempest_time/api/jira_api/responses/get_user_issues.rb +14 -0
- data/lib/tempest_time/api/request.rb +87 -0
- data/lib/tempest_time/api/response.rb +34 -0
- data/lib/tempest_time/api/tempo_api/authorization.rb +23 -0
- data/lib/tempest_time/api/tempo_api/models/worklog.rb +36 -0
- data/lib/tempest_time/api/tempo_api/request.rb +35 -0
- data/lib/tempest_time/api/tempo_api/requests/create_worklog.rb +51 -0
- data/lib/tempest_time/api/tempo_api/requests/delete_worklog.rb +29 -0
- data/lib/tempest_time/api/tempo_api/requests/get_worklog.rb +29 -0
- data/lib/tempest_time/api/tempo_api/requests/list_worklogs.rb +41 -0
- data/lib/tempest_time/api/tempo_api/requests/report.rb +0 -0
- data/lib/tempest_time/api/tempo_api/requests/submit_timesheet.rb +49 -0
- data/lib/tempest_time/api/tempo_api/response.rb +5 -0
- data/lib/tempest_time/api/tempo_api/responses/create_worklog.rb +28 -0
- data/lib/tempest_time/api/tempo_api/responses/delete_worklog.rb +8 -0
- data/lib/tempest_time/api/tempo_api/responses/get_worklog.rb +22 -0
- data/lib/tempest_time/api/tempo_api/responses/list_worklogs.rb +40 -0
- data/lib/tempest_time/api/tempo_api/responses/submit_timesheet.rb +13 -0
- data/lib/tempest_time/cli.rb +92 -0
- data/lib/tempest_time/command.rb +60 -0
- data/lib/tempest_time/commands/config.rb +30 -0
- data/lib/tempest_time/commands/config/edit.rb +38 -0
- data/lib/tempest_time/commands/config/setup.rb +55 -0
- data/lib/tempest_time/commands/delete.rb +31 -0
- data/lib/tempest_time/commands/issues.rb +37 -0
- data/lib/tempest_time/commands/list.rb +32 -0
- data/lib/tempest_time/commands/report.rb +83 -0
- data/lib/tempest_time/commands/submit.rb +26 -0
- data/lib/tempest_time/commands/teams.rb +30 -0
- data/lib/tempest_time/commands/teams/add.rb +29 -0
- data/lib/tempest_time/commands/teams/delete.rb +32 -0
- data/lib/tempest_time/commands/teams/edit.rb +49 -0
- data/lib/tempest_time/commands/track.rb +70 -0
- data/lib/tempest_time/helpers/formatting_helper.rb +16 -0
- data/lib/tempest_time/helpers/time_helper.rb +63 -0
- data/lib/tempest_time/models/report.rb +58 -0
- data/lib/tempest_time/services/generate_report.rb +54 -0
- data/lib/tempest_time/setting.rb +47 -0
- data/lib/tempest_time/settings/authorization.rb +19 -0
- data/lib/tempest_time/settings/teams.rb +19 -0
- data/lib/tempest_time/templates/config/.gitkeep +1 -0
- data/lib/tempest_time/templates/config/auth/.gitkeep +1 -0
- data/lib/tempest_time/templates/config/setup/.gitkeep +1 -0
- data/lib/tempest_time/templates/config/teams/.gitkeep +1 -0
- data/lib/tempest_time/templates/delete/.gitkeep +1 -0
- data/lib/tempest_time/templates/issue/.gitkeep +1 -0
- data/lib/tempest_time/templates/issues/.gitkeep +1 -0
- data/lib/tempest_time/templates/list/.gitkeep +1 -0
- data/lib/tempest_time/templates/report/.gitkeep +1 -0
- data/lib/tempest_time/templates/setup/.gitkeep +1 -0
- data/lib/tempest_time/templates/submit/.gitkeep +1 -0
- data/lib/tempest_time/templates/teams/.gitkeep +1 -0
- data/lib/tempest_time/templates/track/.gitkeep +1 -0
- data/lib/tempest_time/templates/view/.gitkeep +1 -0
- data/lib/tempest_time/version.rb +3 -0
- data/tempest-time.gemspec +62 -0
- metadata +517 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../command'
|
4
|
+
require_relative '../helpers/time_helper'
|
5
|
+
require_relative '../api/tempo_api/requests/list_worklogs'
|
6
|
+
|
7
|
+
module TempestTime
|
8
|
+
module Commands
|
9
|
+
class List < TempestTime::Command
|
10
|
+
include TempestTime::Helpers::TimeHelper
|
11
|
+
|
12
|
+
def initialize(date, options)
|
13
|
+
@date = date
|
14
|
+
@options = options
|
15
|
+
end
|
16
|
+
|
17
|
+
def execute(input: $stdin, output: $stdout)
|
18
|
+
dates = parsed_date_input(@date)
|
19
|
+
dates.each do |start_date|
|
20
|
+
request = TempoAPI::Requests::ListWorklogs.new(
|
21
|
+
start_date,
|
22
|
+
@options['end_date'],
|
23
|
+
@options[:user]
|
24
|
+
)
|
25
|
+
request.send_request
|
26
|
+
puts "\nHere are your logs for #{formatted_date_range(start_date, @options['end_date'])}:\n"
|
27
|
+
puts request.response_message
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../command'
|
4
|
+
require_relative '../settings/teams'
|
5
|
+
require_relative '../services/generate_report'
|
6
|
+
require_relative '../helpers/time_helper'
|
7
|
+
|
8
|
+
module TempestTime
|
9
|
+
module Commands
|
10
|
+
class Report < TempestTime::Command
|
11
|
+
include TempestTime::Helpers::TimeHelper
|
12
|
+
|
13
|
+
def initialize(users, options)
|
14
|
+
@users = users || []
|
15
|
+
@options = options
|
16
|
+
end
|
17
|
+
|
18
|
+
def execute(input: $stdin, output: $stdout)
|
19
|
+
team = @options[:team]
|
20
|
+
@users = prompt_for_input if @users.empty? && team.nil?
|
21
|
+
@users.push(TempestTime::Settings::Teams.members(team)) if team
|
22
|
+
abort('No users specified.') unless @users.any?
|
23
|
+
with_spinner('Generating report...') do |spinner|
|
24
|
+
table = render_table
|
25
|
+
spinner.stop(pastel.green('Your report is ready!'))
|
26
|
+
date_range = "#{formatted_date(report.start_date)}"\
|
27
|
+
' to '\
|
28
|
+
"#{formatted_date(report.end_date)}"
|
29
|
+
prompt.say("\nReport for #{pastel.green(date_range)}")
|
30
|
+
puts table
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def report
|
37
|
+
@report ||= TempestTime::Services::GenerateReport.new(
|
38
|
+
@users.flatten, @options[:week]
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def prompt_for_input
|
43
|
+
type = prompt.select(
|
44
|
+
"Generate a report for a #{pastel.green('user')} or #{pastel.green('team')}?",
|
45
|
+
['User', 'Team']
|
46
|
+
)
|
47
|
+
return [prompt.ask('Which user would you like to analyse?')] if type == 'User'
|
48
|
+
teams = TempestTime::Settings::Teams
|
49
|
+
abort('You have no teams yet! Go make one! (tempest teams add)') unless teams.keys.any?
|
50
|
+
team = prompt.select(
|
51
|
+
"Which #{pastel.green('team')} would you like to analyse?",
|
52
|
+
teams.keys
|
53
|
+
)
|
54
|
+
teams.members(team)
|
55
|
+
end
|
56
|
+
|
57
|
+
def table_headings
|
58
|
+
%w[User COMP% UTIL%] + report.projects
|
59
|
+
end
|
60
|
+
|
61
|
+
def render_table
|
62
|
+
t = table.new(
|
63
|
+
table_headings,
|
64
|
+
report.reports.map { |r| row(r) } + [row(report.aggregate)]
|
65
|
+
)
|
66
|
+
|
67
|
+
t.render(:ascii, padding: [0, 1])
|
68
|
+
end
|
69
|
+
|
70
|
+
def row(r)
|
71
|
+
row = [
|
72
|
+
r.user,
|
73
|
+
r.total_compliance_percentage.round(2),
|
74
|
+
r.utilization_percentage.round(2)
|
75
|
+
]
|
76
|
+
report.projects.each do |project|
|
77
|
+
row.push(r.project_compliance_percentages.to_h.fetch(project, 0).round(2))
|
78
|
+
end
|
79
|
+
row
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../command'
|
4
|
+
require_relative '../api/tempo_api/requests/submit_timesheet'
|
5
|
+
|
6
|
+
module TempestTime
|
7
|
+
module Commands
|
8
|
+
class Submit < TempestTime::Command
|
9
|
+
def initialize(options)
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute(input: $stdin, output: $stdout)
|
14
|
+
# Command logic goes here ...
|
15
|
+
reviewer = prompt.ask('Who should review this timesheet? (username)')
|
16
|
+
message = "Submit this week's timesheet to " + pastel.green(reviewer) + '?'
|
17
|
+
abort unless prompt.yes?(message)
|
18
|
+
abort unless prompt.yes?('Are you sure? No edits can be made once submitted!')
|
19
|
+
|
20
|
+
with_success_fail_spinner("Submitting this week's timesheet...") do
|
21
|
+
TempoAPI::Requests::SubmitTimesheet.new(reviewer).send_request
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
|
5
|
+
module TempestTime
|
6
|
+
module Commands
|
7
|
+
class Teams < Thor
|
8
|
+
|
9
|
+
namespace :config
|
10
|
+
|
11
|
+
desc 'add', 'Add a team.'
|
12
|
+
def add(*)
|
13
|
+
require_relative 'teams/add'
|
14
|
+
TempestTime::Commands::Teams::Add.new(options).execute
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'edit', 'Edit a team.'
|
18
|
+
def edit(*)
|
19
|
+
require_relative 'teams/edit'
|
20
|
+
TempestTime::Commands::Teams::Edit.new(options).execute
|
21
|
+
end
|
22
|
+
|
23
|
+
desc 'delete', 'Edit a team.'
|
24
|
+
def delete(*)
|
25
|
+
require_relative 'teams/delete'
|
26
|
+
TempestTime::Commands::Teams::Delete.new(options).execute
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../command'
|
4
|
+
require_relative '../../settings/teams'
|
5
|
+
|
6
|
+
module TempestTime
|
7
|
+
module Commands
|
8
|
+
class Teams
|
9
|
+
class Add < TempestTime::Command
|
10
|
+
def initialize(options)
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(input: $stdin, output: $stdout)
|
15
|
+
teams = TempestTime::Settings::Teams
|
16
|
+
message =
|
17
|
+
'Please enter ' + pastel.green('the members') + " of this team. "\
|
18
|
+
'(Comma-separated, e.g. jkirk, jpicard, bsisko, kjaneway) '
|
19
|
+
members = prompt.ask(message) do |q|
|
20
|
+
q.convert ->(input) { input.split(/,\s*/) }
|
21
|
+
end
|
22
|
+
name = prompt.ask('Please enter ' + pastel.green('the name') + ' of your new team.')
|
23
|
+
teams.update(name, members)
|
24
|
+
prompt.say(pastel.green('Success!'))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../command'
|
4
|
+
require_relative '../../settings/teams'
|
5
|
+
|
6
|
+
module TempestTime
|
7
|
+
module Commands
|
8
|
+
class Teams
|
9
|
+
class Delete < TempestTime::Command
|
10
|
+
def initialize(options)
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(input: $stdin, output: $stdout)
|
15
|
+
teams = TempestTime::Settings::Teams
|
16
|
+
abort("There are no teams to delete!") unless teams.keys.any?
|
17
|
+
team = prompt.select(
|
18
|
+
"Which #{pastel.green('team')} would you like to delete?",
|
19
|
+
teams.keys
|
20
|
+
)
|
21
|
+
if prompt.yes?(pastel.red("Are you sure you want to delete #{team}?"))
|
22
|
+
teams.delete(team)
|
23
|
+
prompt.say("Successfully #{pastel.red("deleted #{team}!")}")
|
24
|
+
else
|
25
|
+
abort('Nothing was deleted.')
|
26
|
+
end
|
27
|
+
execute if prompt.yes?('Delete another team?')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../command'
|
4
|
+
require_relative '../../settings/teams'
|
5
|
+
|
6
|
+
module TempestTime
|
7
|
+
module Commands
|
8
|
+
class Teams
|
9
|
+
class Edit < TempestTime::Command
|
10
|
+
def initialize(options)
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(input: $stdin, output: $stdout)
|
15
|
+
teams = TempestTime::Settings::Teams
|
16
|
+
abort("There are no teams to edit!") unless teams.keys.any?
|
17
|
+
team = prompt.select(
|
18
|
+
"Which #{pastel.green('team')} would you like to edit?",
|
19
|
+
teams.keys
|
20
|
+
)
|
21
|
+
|
22
|
+
members = teams.members(team)
|
23
|
+
member = prompt.select(
|
24
|
+
"Which #{pastel.green('member')} would you like to edit?",
|
25
|
+
members + ['Add New Member']
|
26
|
+
)
|
27
|
+
|
28
|
+
replace = prompt.ask(
|
29
|
+
"Enter the #{pastel.green('new name')}. "\
|
30
|
+
"Leave blank to #{pastel.red('delete')}."
|
31
|
+
)
|
32
|
+
|
33
|
+
members.delete(member)
|
34
|
+
|
35
|
+
if replace.nil?
|
36
|
+
teams.update(team, members)
|
37
|
+
prompt.say("Deleted #{pastel.red(member)}!")
|
38
|
+
else
|
39
|
+
members.push(replace)
|
40
|
+
teams.update(team, members)
|
41
|
+
prompt.say("Added #{pastel.green(replace)}")
|
42
|
+
end
|
43
|
+
|
44
|
+
execute if prompt.yes?('Keep editing?')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../command'
|
4
|
+
require_relative '../helpers/time_helper'
|
5
|
+
|
6
|
+
require_relative '../api/tempo_api/requests/create_worklog'
|
7
|
+
require_relative '../api/jira_api/requests/get_issue'
|
8
|
+
|
9
|
+
module TempestTime
|
10
|
+
module Commands
|
11
|
+
class Track < TempestTime::Command
|
12
|
+
include TempestTime::Helpers::TimeHelper
|
13
|
+
|
14
|
+
def initialize(time, tickets, options)
|
15
|
+
@time = time
|
16
|
+
@tickets = tickets
|
17
|
+
@options = options
|
18
|
+
end
|
19
|
+
|
20
|
+
def execute(input: $stdin, output: $stdout)
|
21
|
+
time = @options[:split] ? parsed_time(@time) / @tickets.count : parsed_time(@time)
|
22
|
+
tickets = @tickets.any? ? @tickets.map(&:upcase) : [automatic_ticket]
|
23
|
+
|
24
|
+
prompt_message = "Track #{formatted_time(time)}, "\
|
25
|
+
"#{billability(@options)}, "\
|
26
|
+
"to #{tickets.join(', ')}?"
|
27
|
+
abort unless prompt.yes?(prompt_message, convert: :bool)
|
28
|
+
|
29
|
+
tickets.each do |ticket|
|
30
|
+
track_time(time, @options.merge(ticket: ticket))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def track_time(time, options)
|
37
|
+
message = "Tracking #{formatted_time(time)} to #{options['ticket']}..."
|
38
|
+
with_success_fail_spinner(message) do
|
39
|
+
options['remaining'] = if options['remaining'].nil?
|
40
|
+
remaining_estimate(options['ticket'], time)
|
41
|
+
else
|
42
|
+
parsed_time(options['remaining'])
|
43
|
+
end
|
44
|
+
TempoAPI::Requests::CreateWorklog.new(time, options).send_request
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def remaining_estimate(ticket, time)
|
49
|
+
request = JiraAPI::Requests::GetIssue.new(ticket)
|
50
|
+
request.send_request
|
51
|
+
if request.response.failure?
|
52
|
+
abort("There was an issue getting this Jira ticket.\n"\
|
53
|
+
'Please check the ticket number and your credentials.')
|
54
|
+
end
|
55
|
+
remaining = request.response.issue.remaining_estimate || 0
|
56
|
+
remaining > time ? remaining - time : 0
|
57
|
+
end
|
58
|
+
|
59
|
+
def automatic_ticket
|
60
|
+
ticket = /[A-Z]+-\d+/.match(Git.open(Dir.pwd).current_branch)
|
61
|
+
abort('Ticket not found for this branch. Please specify.') unless ticket
|
62
|
+
ticket.to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
def billability(options)
|
66
|
+
options['billable'] ? 'billed' : 'non-billed'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module TempestTime
|
2
|
+
module Helpers
|
3
|
+
module FormattingHelper
|
4
|
+
def braced(text, length = text.length)
|
5
|
+
padding_amount = length - text.length
|
6
|
+
front_padding = ' ' * (padding_amount / 2.0).ceil
|
7
|
+
back_padding = ' ' * (padding_amount / 2.0).floor
|
8
|
+
'[' + front_padding + text + back_padding + ']'
|
9
|
+
end
|
10
|
+
|
11
|
+
def with_percent_sign(decimal)
|
12
|
+
"#{(decimal * 100).to_i}%"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module TempestTime
|
4
|
+
module Helpers
|
5
|
+
module TimeHelper
|
6
|
+
def parsed_time(time)
|
7
|
+
# Returns seconds.
|
8
|
+
return time if time.is_a?(Integer)
|
9
|
+
|
10
|
+
if /^\d*\.{0,1}\d{1,2}h$/.match(time)
|
11
|
+
return (time.chomp('h').to_f * 60 * 60).to_i
|
12
|
+
elsif /^\d+m$/.match(time)
|
13
|
+
return time.chomp('m').to_i * 60
|
14
|
+
end
|
15
|
+
|
16
|
+
abort("Please provide time in the correct format. e.g. 0.5h, .5h, 30m")
|
17
|
+
end
|
18
|
+
|
19
|
+
def formatted_time(seconds)
|
20
|
+
seconds < 3600 ? "#{seconds / 60} minutes" : "#{(seconds / 3600.to_f).round(2)} hours"
|
21
|
+
end
|
22
|
+
|
23
|
+
def formatted_date_range(start_date, end_date)
|
24
|
+
return formatted_date(start_date) if end_date.nil? || end_date.empty?
|
25
|
+
"#{formatted_date(start_date)} - #{formatted_date(end_date)}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def formatted_date(date)
|
29
|
+
"#{Date::DAYNAMES[date.wday]}, #{Date::MONTHNAMES[date.month]} #{date.day}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def parsed_date_input(date_input)
|
33
|
+
case date_input
|
34
|
+
when 'today', nil
|
35
|
+
[Date.today]
|
36
|
+
when 'yesterday'
|
37
|
+
[Date.today.prev_day]
|
38
|
+
when 'week'
|
39
|
+
this_week
|
40
|
+
else
|
41
|
+
[Date.parse(date_input)]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def beginning_of_this_week
|
46
|
+
Date.today - Date.today.wday
|
47
|
+
end
|
48
|
+
|
49
|
+
def beginning_of_week(week_number)
|
50
|
+
this_week_number = (Date.today + 1).cweek # Add one so weeks begin on Sunday.
|
51
|
+
return false unless week_number <= this_week_number
|
52
|
+
days_in_the_past = (this_week_number - week_number) * 7
|
53
|
+
beginning_of_this_week - days_in_the_past
|
54
|
+
end
|
55
|
+
|
56
|
+
def this_week
|
57
|
+
(0..6).map do |n|
|
58
|
+
beginning_of_this_week + n
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require_relative '../helpers/formatting_helper'
|
2
|
+
|
3
|
+
module TempestTime
|
4
|
+
module Models
|
5
|
+
class Report
|
6
|
+
include TempestTime::Helpers::FormattingHelper
|
7
|
+
EXPECTED_SECONDS = (37.5 * 60 * 60).to_i.freeze
|
8
|
+
INTERNAL_PROJECT = 'BCIT'.freeze
|
9
|
+
|
10
|
+
attr_reader :user, :worklogs, :number_of_users
|
11
|
+
|
12
|
+
def initialize(user, worklogs, number_of_users = 1)
|
13
|
+
@user = user
|
14
|
+
@worklogs = worklogs
|
15
|
+
@number_of_users = number_of_users
|
16
|
+
end
|
17
|
+
|
18
|
+
def project_total_times
|
19
|
+
@project_total_times ||= project_worklogs.map do |project, worklogs|
|
20
|
+
[project, time_logged_seconds(worklogs)]
|
21
|
+
end.to_h
|
22
|
+
end
|
23
|
+
|
24
|
+
def compliance_percentage(time)
|
25
|
+
(time.to_f / (EXPECTED_SECONDS * number_of_users)).round(2)
|
26
|
+
end
|
27
|
+
|
28
|
+
def project_compliance_percentages
|
29
|
+
@project_compliance_percentages ||= project_total_times.map do |project, time|
|
30
|
+
[project, compliance_percentage(time)]
|
31
|
+
end.to_h.sort
|
32
|
+
end
|
33
|
+
|
34
|
+
def total_compliance_percentage
|
35
|
+
@total_compliance_percentage ||= compliance_percentage(time_logged_seconds(worklogs))
|
36
|
+
end
|
37
|
+
|
38
|
+
def utilization_percentage
|
39
|
+
@utilization_percentage ||= project_compliance_percentages.inject(0) do |memo, (project, percentage)|
|
40
|
+
memo += percentage unless project == INTERNAL_PROJECT
|
41
|
+
memo
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def time_logged_seconds(logs)
|
46
|
+
logs.flat_map(&:seconds).reduce(:+)
|
47
|
+
end
|
48
|
+
|
49
|
+
def project_worklogs
|
50
|
+
@project_worklogs ||= worklogs.group_by(&:project)
|
51
|
+
end
|
52
|
+
|
53
|
+
def projects
|
54
|
+
@projects ||= project_worklogs.keys.sort
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|