harvest-worklog 0.5.0 → 0.6.0

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: c1397fdec13d0a89c0023e926fa42043f701c351e0fbca487e40331c61bbfbcf
4
- data.tar.gz: '09b9a725ea95df79e94cf204a1f12735c6db8e4d55d4a38f8442fc3f947f981f'
3
+ metadata.gz: 6dcc34ed6ba1cee2e39e8dea71a9b05d80fa42b598366f16332eb1676e197789
4
+ data.tar.gz: 30e312eecb4f435930b74c2aa72a2a68e4f973d22f946e824bb8fee863ae8faa
5
5
  SHA512:
6
- metadata.gz: 33f8bbfa7e4d6f4b98bcc014d6ba3cdb51193a2582a3fd3bed37e3d4051fb606440065f60076d9afd1efc9ab64aa4d6f14a4d6fdb4dd0ff92775419a34012d74
7
- data.tar.gz: 32d59d73fc353d159e7e7a6c93373147f0a7756b4f3b0ba56b4bb877b2cd177000ee8898739eeb1813c3830640cb8a6ce57d1272ab9aa1a5f1bdec1146f3adf9
6
+ metadata.gz: fed0b9fa6998e23a607cb447e108d9849192ebdc209fc263e65ac850f19306a6430f628ccb2c5f8c9548d16d10197793ef2283e917391c2bca553aa12f7a5350
7
+ data.tar.gz: 1fcb49e4868b5c8e3d5eaadd37a0c6961c5be474164a6dc5ddd346f1286e82093b6b6698e176fc95c08a92f785873d7a2e8232b8b7f723daf1a351a3d572467a
data/README.md CHANGED
@@ -22,6 +22,7 @@ Personal Access Tokens are the intended Harvest authentication method for person
22
22
  ```text
23
23
  harvest-worklog time-off FROM TO --project NAME --task NAME [options]
24
24
  harvest-worklog work-entry DATE --project NAME --task NAME --hours HOURS --notes NOTES [options]
25
+ harvest-worklog aggregate FROM TO [--project NAME] [--task NAME]
25
26
  ```
26
27
 
27
28
  ### Time off
@@ -44,6 +45,18 @@ A time-off block never creates weekend entries. When names are supplied, it reso
44
45
 
45
46
  The `work-entry` command checks for existing or locked entries for its project, task, and date before creating a new duration entry. Use `--dry-run` to complete that preflight without a write. OMP Project Time uses the same path.
46
47
 
48
+ ### Read-only aggregates
49
+
50
+ The `aggregate` command reads all matching Harvest time-entry pages and prints totals grouped by spent date and project/task. It lists every date in the inclusive range, including weekend or empty dates, and never writes a Harvest record.
51
+
52
+ ```bash
53
+ harvest-worklog aggregate 2026-07-17 2026-07-19 \
54
+ --project WRAP \
55
+ --task Programming
56
+ ```
57
+
58
+ The `harvest_time_aggregates` OMP tool exposes the same optional filters and is approval-gated as a read.
59
+
47
60
  ## OMP settings
48
61
 
49
62
  - `defaultHours`: hours per business day when a time-off tool call omits `hours`; defaults to `7`.
@@ -80,8 +93,11 @@ harvest-worklog time-off 2026-08-17 2026-08-28 \
80
93
  --task 'Vacation / PTO' \
81
94
  --holiday-region ca_yt \
82
95
  --dry-run
96
+ VERSION="$(ruby -Ilib -rharvest_worklog/version -e 'puts HarvestWorklog::VERSION')"
83
97
  gem build harvest-worklog.gemspec
84
- gem push harvest-worklog-<version>.gem
98
+ gem push "harvest-worklog-${VERSION}.gem"
99
+ gem uninstall harvest-time-off --all --executables --ignore-dependencies
100
+ gem install --clear-sources --source https://rubygems.org harvest-worklog --version "$VERSION" --no-document
85
101
  omp plugin uninstall harvest-time-off
86
102
  omp plugin install --force github:klondikemarlen/harvest-worklog
87
103
  omp plugin list
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HarvestWorklog
4
+ class AggregateCLI
5
+ PER_PAGE = 100
6
+
7
+ def self.run(arguments, output: $stdout, error: $stderr, client: nil)
8
+ options = {}
9
+ parser = option_parser(options)
10
+ dates = parser.parse(arguments)
11
+ validate!(dates)
12
+
13
+ from = Date.iso8601(dates[0])
14
+ to = Date.iso8601(dates[1])
15
+ raise Error, "end date must not be before start date" if to < from
16
+
17
+ client ||= Marlens::HarvestApiV2::Client.from_environment
18
+ entries = fetch_entries(client, from:, to:).select { |entry| matches?(entry, options) }
19
+ print_summary(output, entries, from:, to:)
20
+ 0
21
+ rescue Error, Marlens::HarvestApiV2::Error, OptionParser::ParseError, Date::Error => e
22
+ error.puts "Error: #{e.message}"
23
+ error.puts parser if parser
24
+ 1
25
+ end
26
+
27
+ def self.option_parser(options)
28
+ OptionParser.new do |opts|
29
+ opts.banner = "Usage: harvest-worklog aggregate FROM TO [--project NAME] [--task NAME]"
30
+ opts.on("--project NAME", "Filter by Harvest project name") { |value| options[:project] = value }
31
+ opts.on("--task NAME", "Filter by Harvest task name") { |value| options[:task] = value }
32
+ opts.on("-h", "--help", "Show this help") do
33
+ puts opts
34
+ exit 0
35
+ end
36
+ end
37
+ end
38
+
39
+ def self.validate!(dates)
40
+ raise Error, "FROM and TO are required" unless dates.length == 2
41
+ end
42
+
43
+ def self.fetch_entries(client, from:, to:)
44
+ entries = []
45
+ page = 1
46
+ loop do
47
+ response = client.request(
48
+ :get,
49
+ "/v2/time_entries",
50
+ params: { from: from.iso8601, to: to.iso8601, page:, per_page: PER_PAGE }
51
+ )
52
+ entries.concat(response.fetch("time_entries"))
53
+ page = response["next_page"]
54
+ break if page.nil?
55
+ end
56
+ entries
57
+ end
58
+
59
+ def self.matches?(entry, options)
60
+ name_matches?(entry.dig("project", "name"), options[:project]) &&
61
+ name_matches?(entry.dig("task", "name"), options[:task])
62
+ end
63
+
64
+ def self.name_matches?(actual, expected)
65
+ expected.nil? || actual&.casecmp?(expected)
66
+ end
67
+
68
+ def self.print_summary(output, entries, from:, to:)
69
+ output.puts "#{entries.length} #{entry_label(entries.length)}, #{HarvestWorklog.display_hours(total_hours(entries))}h from #{from.iso8601} through #{to.iso8601}"
70
+ output.puts "By date:"
71
+ (from..to).each do |date|
72
+ date_entries = entries.select { |entry| entry.fetch("spent_date") == date.iso8601 }
73
+ output.puts " #{date.iso8601}: #{date_entries.length} #{entry_label(date_entries.length)}, #{HarvestWorklog.display_hours(total_hours(date_entries))}h"
74
+ end
75
+ output.puts "By project/task:"
76
+
77
+ groups = entries.group_by { |entry| [entry.dig("project", "name"), entry.dig("task", "name")] }
78
+ if groups.empty?
79
+ output.puts " none"
80
+ return
81
+ end
82
+
83
+ groups.keys.sort_by { |project, task| [project.downcase, task.downcase] }.each do |project, task|
84
+ group = groups.fetch([project, task])
85
+ output.puts " #{project} / #{task}: #{group.length} #{entry_label(group.length)}, #{HarvestWorklog.display_hours(total_hours(group))}h"
86
+ end
87
+ end
88
+
89
+ def self.total_hours(entries)
90
+ entries.sum { |entry| Float(entry.fetch("hours")) }
91
+ end
92
+
93
+ def self.entry_label(count)
94
+ count == 1 ? "entry" : "entries"
95
+ end
96
+ end
97
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HarvestWorklog
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
@@ -34,11 +34,13 @@ module HarvestWorklog
34
34
  TimeOffCLI.run(command_arguments, output:, error:, client:)
35
35
  when "work-entry"
36
36
  WorkEntryCLI.run(command_arguments, output:, error:, client:)
37
+ when "aggregate"
38
+ AggregateCLI.run(command_arguments, output:, error:, client:)
37
39
  when "-h", "--help"
38
40
  output.puts usage
39
41
  0
40
42
  else
41
- error.puts "Error: choose time-off or work-entry"
43
+ error.puts "Error: choose time-off, work-entry, or aggregate"
42
44
  error.puts usage
43
45
  1
44
46
  end
@@ -49,10 +51,12 @@ module HarvestWorklog
49
51
  Usage:
50
52
  harvest-worklog time-off FROM TO --project NAME --task NAME [options]
51
53
  harvest-worklog work-entry DATE --project NAME --task NAME --hours HOURS --notes NOTES [options]
54
+ harvest-worklog aggregate FROM TO [--project NAME] [--task NAME]
52
55
 
53
56
  Commands:
54
57
  time-off Create one entry per local business day in a date range.
55
58
  work-entry Create one reviewed ordinary-work entry.
59
+ aggregate Read time-entry totals without writing Harvest records.
56
60
  USAGE
57
61
  end
58
62
  end
@@ -165,3 +169,4 @@ module HarvestWorklog
165
169
  end
166
170
 
167
171
  require "harvest_worklog/work_entry_cli"
172
+ require "harvest_worklog/aggregate_cli"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: harvest-worklog
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marlen Brunner
@@ -62,6 +62,7 @@ files:
62
62
  - bin/harvest-worklog
63
63
  - harvest-worklog.rb
64
64
  - lib/harvest_worklog.rb
65
+ - lib/harvest_worklog/aggregate_cli.rb
65
66
  - lib/harvest_worklog/version.rb
66
67
  - lib/harvest_worklog/work_entry_cli.rb
67
68
  homepage: https://github.com/klondikemarlen/harvest-worklog