twstats 0.3.0 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e126990ce8c73929882e7246c5a77cce582c46fd115b4241b71f12b17e1e6f6b
4
- data.tar.gz: 9184395243e7c2b7305248e724027eca9c84cab170cc6b742bdb0a893e484b69
3
+ metadata.gz: a3332529ad2297075381d84b5deb01dd0bd0d70f87796fa13788da6be1476e54
4
+ data.tar.gz: 5d07d48ff3f08b8f093ee9b9167bead08559207bbfdc77a8cd0747ac893ae1b9
5
5
  SHA512:
6
- metadata.gz: 97271cf581f7b696aa23be141ddc3800fe42089803ba999f32f74b0ef9e37bd0577bf55c5bf8ee4e81d1b5257e009725d20f7e9b96db33a2b6390a7d5c5c2cbc
7
- data.tar.gz: 9fa944b7a95d245a9c285196893549b2d6f6f804406003d98ccac50878117160b345b31fbd07f13e8ce9473683ecf6fdc0ccc8d9d02dd66dcaaec222476a96fe
6
+ metadata.gz: a493425c5bb3b043f12c72f6426a92a63c3b9920fc4f092135830b1ee66d1f478f186099611cf18bef4bfc89415e375b9a3c9b32da6e6bd56a88c48c37e26530
7
+ data.tar.gz: c286353bf589221a812a994272cadb7b71750bbc5bfa1bcf82b44d0aab8de793ebcc3031f1f446a00b2e61d38c52423f8f0400b5c6ea6f9bc6279ed01bba41dc
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- twstats (0.2.5)
4
+ twstats (0.3.0)
5
5
  descriptive-statistics (>= 2.2.0)
6
6
  rainbow (~> 3.0)
7
7
  tty-prompt (~> 0.17)
@@ -4,6 +4,7 @@ require_relative 'twstats/tw_log'
4
4
  require_relative 'twstats/csv_reader'
5
5
  require_relative 'twstats/descriptive_stats'
6
6
  require_relative 'twstats/timesheet_export'
7
+ require_relative 'twstats/hourly_rate'
7
8
  require_relative 'twstats/runner'
8
9
 
9
10
  module Twstats
@@ -21,7 +22,8 @@ module Twstats
21
22
  ]
22
23
  MENU_CHOICES = [
23
24
  {name: 'Stats', value: :stats},
24
- {name: 'TimeSheet', value: :billing},
25
+ {name: 'TimeSheet', value: :timesheet},
26
+ {name: 'Billing', value: :billing},
25
27
  {name: 'Weekly', value: :weekly},
26
28
  {name: 'Quit', value: :quit}
27
29
  ]
@@ -36,23 +36,23 @@ module Twstats
36
36
  billable = 0
37
37
  non_billable = 0
38
38
  case object
39
- when :projects
40
- billable = logs.select{|log| log.project == element && log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
41
- non_billable = logs.select{|log| log.project == element && !log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
42
- when :tags
43
- billable = logs.select{|log| log.tags.include?(element) && log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
44
- non_billable = logs.select{|log| log.tags.include?(element) && !log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
45
- when :people
46
- billable = logs.select{|log| log.who == element && log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
47
- non_billable = logs.select{|log| log.who == element && !log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
48
- when :tasks
49
- billable = logs.select{|log| log.task == element && log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
50
- non_billable = logs.select{|log| log.task == element && !log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
51
- when :all
52
- billable = logs.select{|log| log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
53
- non_billable = logs.select{|log| !log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
54
- else
55
- return -99999
39
+ when :projects
40
+ billable = logs.select{|log| log.project == element && log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
41
+ non_billable = logs.select{|log| log.project == element && !log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
42
+ when :tags
43
+ billable = logs.select{|log| log.tags.include?(element) && log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
44
+ non_billable = logs.select{|log| log.tags.include?(element) && !log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
45
+ when :people
46
+ billable = logs.select{|log| log.who == element && log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
47
+ non_billable = logs.select{|log| log.who == element && !log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
48
+ when :tasks
49
+ billable = logs.select{|log| log.task == element && log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
50
+ non_billable = logs.select{|log| log.task == element && !log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
51
+ when :all
52
+ billable = logs.select{|log| log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
53
+ non_billable = logs.select{|log| !log.billable}.inject(0){ |sum, l| sum + l.decimal_time}
54
+ else
55
+ return -99999
56
56
  end
57
57
  if filter_billable
58
58
  return [billable, non_billable]
@@ -66,18 +66,37 @@ module Twstats
66
66
  (sorted_logs[-1] - sorted_logs[0]).to_i <= 7
67
67
  end
68
68
 
69
- def not_tagged_tasks
69
+ ## Filer the logs in the CSV by a given object when the element provided matches the object
70
+ def get_logs(object, element = nil)
71
+ case object
72
+ when :projects
73
+ logs.select{|log| log.project == element}
74
+ when :tags
75
+ logs.select{|log| log.tags.include?(element)}
76
+ when :people
77
+ logs.select{|log| log.who == element}
78
+ when :tasks
79
+ logs.select{|log| log.task == element}
80
+ when :all
81
+ logs
82
+ else
83
+ nil
84
+ end
85
+ end
86
+
87
+ def not_tagged_tasks(to_analize=nil)
88
+ to_analize = logs if to_analize.nil?
70
89
  total_time = 0
71
90
  list = []
72
- logs.each do |log|
91
+ to_analize.each do |log|
73
92
  if log.tags.empty?
74
93
  list << log
75
94
  total_time += log.decimal_time
76
95
  end
77
96
  end
78
97
  {
79
- total_time: total_time,
80
- list: list
98
+ total_time: total_time,
99
+ list: list
81
100
  }
82
101
  end
83
102
  end
@@ -19,5 +19,13 @@ module Twstats
19
19
  end
20
20
  DescriptiveStatistics::Stats.new(time_per_task).mean
21
21
  end
22
+
23
+ def max
24
+ logs.map{|l| l.decimal_time}.max
25
+ end
26
+
27
+ def min
28
+ logs.map{|l| l.decimal_time}.min
29
+ end
22
30
  end
23
- end
31
+ end
@@ -0,0 +1,58 @@
1
+ # This class parses the hourly rate for each employee found in the csv.
2
+ # The first time it ask the user what the price is and saved it to a yml file with all the information needed
3
+ # This information is retrieved
4
+ require 'yaml'
5
+ class HourlyRate
6
+
7
+ def initialize(csv)
8
+ # Read users from file
9
+ @config_file = "#{ENV['HOME']}/.twstats_users.yml"
10
+ @users = csv.people
11
+ @config = {}
12
+ if File.exists? @config_file
13
+ @config = YAML.load(File.read(@config_file))
14
+ end
15
+ # Set value of 0 to users that do not exist
16
+ csv.people.each do |user|
17
+ if @config[user].nil?
18
+ @config[user] = 0
19
+ end
20
+ end
21
+ end
22
+
23
+ def get_price(user,hours)
24
+ # Get a given price for a given user
25
+ end
26
+
27
+ def set_price(user,price)
28
+ @config[user] = price.round(2)
29
+ end
30
+
31
+ def save
32
+ file = File.new(@config_file,'w')
33
+ file.write YAML.dump(@config)
34
+ file.close
35
+ end
36
+
37
+ def get_billed_time(users_hours)
38
+ # It expects a hash with the amount of hours billed per user
39
+ users_hours.keys.inject(0) do |sum, user|
40
+ sum + @config[user]*users_hours[user]
41
+ end
42
+ end
43
+
44
+ def set_user_hourly_rate(prompt)
45
+ menu = @users.map{ |user| ["#{user} (#{@config[user].round(2)} €)", user]}.to_h
46
+ menu['Quit'] = -1
47
+ loop do
48
+ chosen = prompt.select 'What user you want to modify? Select Quit to finish.', menu
49
+ break if chosen == -1
50
+ price = prompt.ask "What is the hourly rate for #{chosen}?", convert: :float
51
+ puts price, chosen
52
+ @config[chosen] = price
53
+ menu = @users.map{ |user| ["#{user} (#{@config[user].round(2)} €)", user]}.to_h
54
+ menu['Quit'] = -1
55
+ end
56
+ save
57
+ end
58
+ end
@@ -14,19 +14,22 @@ module Twstats
14
14
  file = read_file file
15
15
  # Check if the file exists
16
16
  @csv = CSVReader.new(file)
17
+ @hourly_rate = HourlyRate.new(@csv)
17
18
  loop do
18
- option = @prompt.select("Choose an option", Twstats::MENU_CHOICES, cycle: true)
19
+ option = @prompt.select('Choose an option', Twstats::MENU_CHOICES, cycle: true)
19
20
  case option
20
- when :stats
21
- show_stats_menu
22
- when :billing
23
- billing
24
- when :weekly
25
- show_weekly_report
26
- when :quit
27
- break
28
- else
29
- puts 'Option not recognized!'
21
+ when :stats
22
+ show_stats_menu
23
+ when :timesheet
24
+ timesheet
25
+ when :weekly
26
+ show_weekly_report
27
+ when :billing
28
+ billing
29
+ when :quit
30
+ break
31
+ else
32
+ puts 'Option not recognized!'
30
33
  end
31
34
  end
32
35
  @current = nil
@@ -35,13 +38,13 @@ module Twstats
35
38
  def read_file(file = nil)
36
39
  # Check if the file exsis
37
40
  unless file.nil?
38
- if File.exists? file
41
+ if File.exist? file
39
42
  return file
40
43
  else
41
44
  file = @prompt.ask('The file you have supplied does not exsists. Try again or type .q to exit twstats.') do |input|
42
45
  input.modify :chomp
43
46
  end
44
- if file == ".q"
47
+ if file == '.q'
45
48
  exit 0
46
49
  else
47
50
  read_file file
@@ -55,58 +58,88 @@ module Twstats
55
58
  read_file file
56
59
  end
57
60
 
58
- def billing
61
+ def timesheet
59
62
  ## Takes care of using the current CSV file to be imported into timesheets.
60
63
  # Makes it easier to do it than manually going through all of them
61
64
  TimeSheetExport.new(@csv, @prompt).execute
62
65
  end
63
66
 
67
+ def billing
68
+ add_non_billable = @prompt.yes? 'Do you want to include non-billable time?'
69
+ @hourly_rate.set_user_hourly_rate(@prompt)
70
+ if @prompt.yes? 'Was it a closed quote?', default: true
71
+ quoted_price = @prompt.ask 'What was the quoted price for this project?', convert: :float
72
+ end
73
+ price = @hourly_rate.get_billed_time(user_hours_billed(add_non_billable))
74
+ puts ' - Billed amount '.bright.blue + ' | ' + price.round(2).to_s + ' € '
75
+ if quoted_price
76
+ diff = quoted_price - price
77
+ if diff >= 0
78
+ puts " - The project has a proffit of: #{diff.round(2)} €, #{(diff * 100 / quoted_price).round(2)}".bright.green
79
+ else
80
+ puts " - The project shows a loss: #{diff.round(2)} €, #{(diff * 100 / quoted_price).round(2)}%".bright.red
81
+ end
82
+ alltime = @csv.get_total_time(:all, nil, !add_non_billable)
83
+ flat_price = add_non_billable ? (alltime * 46).round(2) : (alltime[0] * 46).round(2)
84
+ puts alltime, flat_price
85
+ diff = quoted_price - flat_price
86
+ if diff >= 0
87
+ puts "The project is profitable using a flat rate of 46 €/hour by #{quoted_price - flat_price}".bright.green
88
+ else
89
+ puts "The project should have been quoted in #{flat_price} using the flat rate of 46 €/hour".bright.red
90
+ end
91
+ end
92
+ end
93
+
64
94
  def show_stats_menu
65
95
  loop do
66
- option = @prompt.select("Select what time logging stats you want to see", Twstats::STATS_MENU_CHOICES, cycle: true)
96
+ option = @prompt.select('Select what time logging stats you want to see', Twstats::STATS_MENU_CHOICES, cycle: true)
67
97
  case option
68
- when :projects, :people, :tags, :tasks
69
- show_stats option
70
- when :fullstats
71
- show_full_stats
72
- when :back
73
- return
74
- else
75
- puts 'Option not recognized'
98
+ when :projects, :people, :tags, :tasks
99
+ show_stats option
100
+ when :fullstats
101
+ show_full_stats
102
+ when :back
103
+ return
104
+ else
105
+ puts 'Option not recognized'
76
106
  end
77
107
  end
78
108
  end
79
109
 
80
110
  def show_stats(obj, filter_billable = nil)
81
- puts "Time logged vs #{obj.to_s}:" if filter_billable.nil?
111
+ puts "Time logged vs #{obj}:" if filter_billable.nil?
82
112
  toshow = {}
83
113
  max = 0
84
114
  filter_billable ||= @prompt.yes?('Do you want to filter billable and non-billable time?', default: true)
115
+ do_all = @prompt.yes? 'Do you want to analyze all the elements?', default: true
85
116
  objects = @csv.send(obj)
86
- if objects.size > 1
87
- list = @prompt.multi_select "Which ones of the following do you want to see? Select or unselect them with the space key\n" do |menu|
117
+ list = objects
118
+ if objects.size > 1 && !do_all
119
+ list = @prompt.multi_select "Which ones of the following do you want to see? Select or unselect them with the space key\n", filter: true do |menu|
88
120
  menu.choices objects
89
- menu.default *[*1..objects.size]
90
121
  end
91
122
  end
123
+ selected_logs = []
92
124
  list.each do |element|
93
125
  toshow[element] = @csv.get_total_time(obj, element, filter_billable)
94
126
  max = element.size if max < element.size
127
+ selected_logs += @csv.get_logs(obj, element)
95
128
  end
96
- toshow.sort_by{ |k,v| v}.reverse.to_h.each do |k,v|
129
+ toshow.sort_by { |_k, v| v }.reverse.to_h.each do |k, v|
97
130
  if filter_billable
98
131
  if v[0].zero?
99
- puts " - #{k.ljust(max,' ').bright.blue} | #{"Non-billable:".bright} #{v[1].round(2)}"# unless v[1].zero?
132
+ puts " - #{k.ljust(max, ' ').bright.blue} | #{'Non-billable:'.bright} #{v[1].round(2)}" # unless v[1].zero?
100
133
  else
101
- puts " - #{k.ljust(max,' ').bright.blue} | #{"Billable:".ljust(13," ").bright} #{v[0].round(2)} (#{((v[0]/(v[0]+v[1]))*100).round(2)} %)"
102
- puts " #{"".ljust(max,' ').bright.blue} | #{"Non-billable:".bright} #{v[1].round(2)}" unless v[1].zero?
134
+ puts " - #{k.ljust(max, ' ').bright.blue} | #{'Billable:'.ljust(13, ' ').bright} #{v[0].round(2)} (#{((v[0] / (v[0] + v[1])) * 100).round(2)} %)"
135
+ puts " #{''.ljust(max, ' ').bright.blue} | #{'Non-billable:'.bright} #{v[1].round(2)}" unless v[1].zero?
103
136
  end
104
137
  else
105
- puts " - #{k.ljust(max,' ').bright.blue} | #{v.round(2)}" unless v.zero?
138
+ puts " - #{k.ljust(max, ' ').bright.blue} | #{v.round(2)}" unless v.zero?
106
139
  end
107
140
  end
108
- show_not_tagged_section false
109
- # Further filtering
141
+ show_not_tagged_section selected_logs, false
142
+ show_metrics selected_logs
110
143
  end
111
144
 
112
145
  def ranking_from_not_tagged(list)
@@ -115,91 +148,83 @@ module Twstats
115
148
  result[log.who] ||= 0
116
149
  result[log.who] += log.decimal_time
117
150
  end
118
- result.sort_by{|k,v| v}.reverse.to_h
151
+ result.sort_by { |_k, v| v }.reverse.to_h
119
152
  end
120
153
 
121
154
  def print_table(log_list)
122
- 167.times{print '-'}
123
- puts "\n|"+'Date'.center(12,' ').bright+'|'+'Time'.center(6,' ').bright+'|'+'Who'.center(20, ' ').bright+
124
- '|'+'Description'.center(78,' ').bright+'|'+'Tags'.center(24,' ').bright+'|'+'Task'.center(20, ' ').bright+'|'
125
- 167.times{print '-'}
126
- puts ""
155
+ 167.times { print '-' }
156
+ puts "\n|" + 'Date'.center(12, ' ').bright + '|' + 'Time'.center(6, ' ').bright + '|' + 'Who'.center(20, ' ').bright +
157
+ '|' + 'Description'.center(78, ' ').bright + '|' + 'Tags'.center(24, ' ').bright + '|' + 'Task'.center(20, ' ').bright + '|'
158
+ 167.times { print '-' }
159
+ puts ''
127
160
  log_list.each do |log|
128
161
  puts table_line(log)
129
162
  end
130
- 167.times{print '-'}
131
- puts ""
163
+ 167.times { print '-' }
164
+ puts ''
132
165
  end
133
166
 
134
167
  def table_line(log)
135
- "|"+"#{log.date.strftime('%d/%m/%Y')}".truncate(10).center(12,' ')+
136
- '|'+"#{log.decimal_time.round(2)}".truncate(4).center(6,' ')+
137
- '|'+"#{log.who}".truncate(18).center(20, ' ')+
138
- '|'+"#{log.description}".truncate(76).ljust(78,' ')+
139
- '|'+"#{log.tags.join(', ')}".truncate(22).center(24,' ')+
140
- '|'+"#{log.task}".truncate(18).center(20, ' ')+'|'
168
+ '|' + log.date.strftime('%d/%m/%Y').to_s.truncate(10).center(12, ' ') +
169
+ '|' + log.decimal_time.round(2).to_s.truncate(4).center(6, ' ') +
170
+ '|' + log.who.to_s.truncate(18).center(20, ' ') +
171
+ '|' + log.description.to_s.truncate(76).ljust(78, ' ').tr("\n", ' ') +
172
+ '|' + log.tags.join(', ').to_s.truncate(22).center(24, ' ') +
173
+ '|' + log.task.to_s.truncate(18).center(20, ' ') + '|'
141
174
  end
142
175
 
143
176
  def show_full_stats
144
177
  billable, non_billable = @csv.get_total_time(:all, nil, true)
145
- to_bill = @prompt.yes? 'Do you want to calculate the billed amount?'
146
- if to_bill
147
- amount_per_hour = @prompt.ask 'What is the hourly rate?', default: 46
148
- if @prompt.yes? 'Was it a closed quote?', default: true
149
- quoted_price = @prompt.ask 'What was the quoted price for this project?', convert: :float
150
- end
151
- end
152
- section "Total time spent"
153
- puts " - Billable ".bright.blue + " | " + billable.round(2).to_s
154
- puts " - Non-billable ".bright.blue + " | " + non_billable.round(2).to_s
155
- if to_bill
156
- price = billable*amount_per_hour
157
- puts " - Billed amount ".bright.blue + " | " + price.round(2).to_s + " € "
158
- if quoted_price
159
- diff = quoted_price-price
160
- if diff >= 0
161
- puts " - Project over estimated: #{diff.round(2)} €, #{diff/price}".bright.green
162
- else diff < 0
163
- puts " - Project under estimated: #{diff.round(2)} €, #{(diff*100/price).round(2)}%".bright.red
164
- end
165
- end
166
- end
167
- section "People involved"
178
+ section 'Total time spent'
179
+ puts ' - Billable '.bright.blue + ' | ' + billable.round(2).to_s
180
+ puts ' - Non-billable '.bright.blue + ' | ' + non_billable.round(2).to_s
181
+ section 'People involved'
168
182
  show_stats :people, true
169
- section "Tags used"
183
+ section 'Tags used'
170
184
  show_stats :tags, true
171
- show_not_tagged_section(true)
172
- section "Usefull metrics"
185
+ show_not_tagged_section(@csv.logs, true)
173
186
  show_metrics
174
187
  end
175
188
 
176
- def show_not_tagged_section(table)
177
- not_tagged = @csv.not_tagged_tasks
189
+ def user_hours_billed(nonb)
190
+ uh = {}
191
+ @csv.people.each do |user|
192
+ val = @csv.get_total_time :people, user, !nonb
193
+ uh[user] = val
194
+ uh[user] = val[0] unless nonb
195
+ end
196
+ uh
197
+ end
198
+
199
+ def show_not_tagged_section(logs, table)
200
+ not_tagged = @csv.not_tagged_tasks logs
178
201
  unless not_tagged[:list].empty?
179
- section("Logs not tagged in this report")
202
+ section('Logs not tagged in this report')
180
203
  puts "A total of #{not_tagged[:list].size} logs have not been tagged:"
181
204
  puts " - A total time of #{not_tagged[:total_time]} is not tagged properly."
182
- puts " - Impact by employee: "
205
+ puts ' - Impact by employee: '
183
206
  ranking_from_not_tagged(not_tagged).each do |user, time|
184
- puts "\t - #{user.ljust(30,' ').bright.blue} | #{time}"
207
+ puts "\t - #{user.ljust(30, ' ').bright.blue} | #{time}"
185
208
  end
186
209
  table = @prompt.yes?('Do you want to see what tasks have not been tagged?', default: true) if table.nil?
187
- if table
188
- print_table not_tagged[:list]
189
- end
210
+ print_table not_tagged[:list] if table
190
211
  end
191
212
  end
192
213
 
193
214
  def section(text)
194
- puts ""
195
- puts (" "+text+" ").center(70,"*").bright.orange
215
+ puts ''
216
+ puts (' ' + text + ' ').center(70, '*').bright.orange
196
217
  end
197
218
 
198
- def show_metrics
199
- stats = LogStats.new(@csv.logs)
200
- puts " - Mean time logged: ".bright.blue + stats.mean_log_time.round(2).to_s
201
- puts " - Mean time per task: ".bright.blue + stats.mean_task_time(@csv.tasks).round(2).to_s
202
- puts " - With a total of #{@csv.tasks.size} tasks"
219
+ def show_metrics(logs = nil)
220
+ logs = @csv.logs if logs.nil?
221
+ stats = LogStats.new(logs)
222
+ section 'Usefull metrics for the selected logs'
223
+ puts "A total of #{logs.size} time logs have been analyzed."
224
+ puts ' - Mean time logged: '.bright.blue + stats.mean_log_time.round(2).to_s
225
+ puts ' - Mean time per task: '.bright.blue + stats.mean_task_time(@csv.tasks).round(2).to_s
226
+ puts ' - Maximum time logged: '.bright.blue + stats.max.to_s
227
+ puts ' - Minimum time logged: '.bright.blue + stats.min.to_s
203
228
  end
204
229
 
205
230
  def show_weekly_report
@@ -213,14 +238,13 @@ module Twstats
213
238
  info = {}
214
239
  @csv.people.each do |person|
215
240
  billable, non_billable = @csv.get_total_time(:people, person, true)
216
- info[person] = {rate: billable * 100 / hours,
217
- not_billed: hours - billable - non_billable,
218
- billable: billable,
219
- non_billable: non_billable
220
- }
221
- end
222
- info.sort_by{|x,v| v[:rate] }.reverse.each do |person, data|
223
- puts " - " + person.bright.blue
241
+ info[person] = { rate: billable * 100 / hours,
242
+ not_billed: hours - billable - non_billable,
243
+ billable: billable,
244
+ non_billable: non_billable }
245
+ end
246
+ info.sort_by { |_x, v| v[:rate] }.reverse_each do |person, data|
247
+ puts ' - ' + person.bright.blue
224
248
  puts "\tBillable time: ".ljust(20, ' ').bright + data[:billable].round(2).to_s
225
249
  puts "\tBillable rate: ".ljust(20, ' ').bright + data[:rate].round(2).to_s + ' % '
226
250
  puts "\tNot logged time: ".ljust(20, ' ').bright + data[:not_billed].round(2).to_s
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: twstats
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - J.P. Araque
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-09-17 00:00:00.000000000 Z
11
+ date: 2018-12-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -120,6 +120,7 @@ files:
120
120
  - lib/twstats/csv_reader.rb
121
121
  - lib/twstats/descriptive_stats.rb
122
122
  - lib/twstats/extensions.rb
123
+ - lib/twstats/hourly_rate.rb
123
124
  - lib/twstats/runner.rb
124
125
  - lib/twstats/timesheet_export.rb
125
126
  - lib/twstats/tw_log.rb
@@ -146,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
146
147
  version: '0'
147
148
  requirements: []
148
149
  rubyforge_project:
149
- rubygems_version: 2.7.7
150
+ rubygems_version: 2.7.8
150
151
  signing_key:
151
152
  specification_version: 4
152
153
  summary: Read a csv file generate by Teamwork to quickly get some stats from time