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 +4 -4
- data/Gemfile.lock +1 -1
- data/lib/twstats.rb +3 -1
- data/lib/twstats/csv_reader.rb +40 -21
- data/lib/twstats/descriptive_stats.rb +9 -1
- data/lib/twstats/hourly_rate.rb +58 -0
- data/lib/twstats/runner.rb +121 -97
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a3332529ad2297075381d84b5deb01dd0bd0d70f87796fa13788da6be1476e54
|
4
|
+
data.tar.gz: 5d07d48ff3f08b8f093ee9b9167bead08559207bbfdc77a8cd0747ac893ae1b9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a493425c5bb3b043f12c72f6426a92a63c3b9920fc4f092135830b1ee66d1f478f186099611cf18bef4bfc89415e375b9a3c9b32da6e6bd56a88c48c37e26530
|
7
|
+
data.tar.gz: c286353bf589221a812a994272cadb7b71750bbc5bfa1bcf82b44d0aab8de793ebcc3031f1f446a00b2e61d38c52423f8f0400b5c6ea6f9bc6279ed01bba41dc
|
data/Gemfile.lock
CHANGED
data/lib/twstats.rb
CHANGED
@@ -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: :
|
25
|
+
{name: 'TimeSheet', value: :timesheet},
|
26
|
+
{name: 'Billing', value: :billing},
|
25
27
|
{name: 'Weekly', value: :weekly},
|
26
28
|
{name: 'Quit', value: :quit}
|
27
29
|
]
|
data/lib/twstats/csv_reader.rb
CHANGED
@@ -36,23 +36,23 @@ module Twstats
|
|
36
36
|
billable = 0
|
37
37
|
non_billable = 0
|
38
38
|
case object
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
80
|
-
|
98
|
+
total_time: total_time,
|
99
|
+
list: list
|
81
100
|
}
|
82
101
|
end
|
83
102
|
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
|
data/lib/twstats/runner.rb
CHANGED
@@ -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(
|
19
|
+
option = @prompt.select('Choose an option', Twstats::MENU_CHOICES, cycle: true)
|
19
20
|
case option
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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.
|
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 ==
|
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
|
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(
|
96
|
+
option = @prompt.select('Select what time logging stats you want to see', Twstats::STATS_MENU_CHOICES, cycle: true)
|
67
97
|
case option
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
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
|
-
|
87
|
-
|
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{ |
|
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} | #{
|
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} | #{
|
102
|
-
puts " #{
|
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
|
-
|
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{|
|
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
|
-
|
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
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
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
|
177
|
-
|
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(
|
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
|
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 (
|
215
|
+
puts ''
|
216
|
+
puts (' ' + text + ' ').center(70, '*').bright.orange
|
196
217
|
end
|
197
218
|
|
198
|
-
def show_metrics
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
puts "
|
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
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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.
|
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-
|
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.
|
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
|