tempest-time 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|