twstats 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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