-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgenerate_summary.rb
More file actions
126 lines (105 loc) · 3.75 KB
/
generate_summary.rb
File metadata and controls
126 lines (105 loc) · 3.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
require_relative 'export'
require 'json'
require 'date'
require 'csv'
class SummaryGenerator
def initialize(history_dir = 'history')
@history_dir = history_dir
@exporter = EarlyExporter.new(test_mode: true) # Use test mode to access private methods
end
def generate(output_file = 'web/public/history_summary.json')
backfill_missing_months if ENV['EARLY_API_KEY'] && ENV['EARLY_API_SECRET']
files = Dir.glob(File.join(@history_dir, '*_history.csv')).sort
monthly_data = files.map do |file|
filename = File.basename(file)
if filename =~ /^(\d{4})_(\d{2})_history\.csv$/
year = $1.to_i
month = $2.to_i
process_file(file, year, month)
end
end.compact
# Calculate moving averages (4-month window)
monthly_data.each_with_index do |data, i|
if i >= 3
window = monthly_data[(i-3)..i]
avg_delta = window.map { |d| d[:hours_diff] }.sum / 4.0
data[:moving_avg_4m] = avg_delta
else
data[:moving_avg_4m] = nil
end
end
result = {
months: monthly_data,
generated_at: Time.now.iso8601
}
File.write(output_file, JSON.pretty_generate(result))
puts "Wrote summary to #{output_file}"
end
private
def backfill_missing_months
files = Dir.glob(File.join(@history_dir, '*_history.csv')).sort
return if files.empty?
# Find the range of months we have
first_file = File.basename(files.first)
return unless first_file =~ /^(\d{4})_(\d{2})_history\.csv$/
start_date = Date.new($1.to_i, $2.to_i, 1)
# End date is the first of THIS month (so we check up to LAST month)
end_date = Date.new(Date.today.year, Date.today.month, 1)
current = start_date
while current < end_date
file_path = File.join(@history_dir, "#{current.strftime('%Y_%m')}_history.csv")
unless File.exist?(file_path)
puts "Backfilling missing history for #{current.strftime('%B %Y')}..."
begin
exporter = EarlyExporter.new(
output_file: file_path,
include_nonbillable: false
)
exporter.run("#{current.year} #{current.month}")
rescue => e
puts "Failed to backfill #{current.strftime('%Y_%m')}: #{e.message}"
end
end
current = current.next_month
end
end
def process_file(file, year, month)
start_date = Date.new(year, month, 1)
end_date = Date.new(year, month, -1)
# We need to know how many weekdays are in the full month
# But wait, if it's the CURRENT month, we might want to only count up to today
# For historical files, they are usually full months.
total_hours = 0.0
CSV.foreach(file, headers: true) do |row|
duration = row['Duration']
total_hours += @exporter.parse_duration_to_hours(duration)
end
# Use the logic from EarlyExporter to be consistent
# Preserve the "buggy" 8-hour discount for months before April 2026
# so we don't destroy historical comp time balances.
if year < 2026 || (year == 2026 && month < 4)
eff_end = end_date
else
eff_end = end_date.next_day
end
if Date.today.year == year && Date.today.month == month
eff_end = Date.today.next_day if Date.today <= end_date
end
weekdays = @exporter.count_weekdays(start_date, eff_end)
expected_hours = weekdays * 8.0
hours_diff = total_hours - expected_hours
{
year: year,
month: month,
total_hours: total_hours.round(2),
expected_hours: expected_hours.round(2),
hours_diff: hours_diff.round(2),
percentage: expected_hours > 0 ? (total_hours / expected_hours * 100).round(1) : 0,
weekdays: weekdays
}
end
end
if __FILE__ == $0
generator = SummaryGenerator.new
generator.generate
end