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 +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
|