basecamp-sdk 0.2.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.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +14 -0
  3. data/.yardopts +6 -0
  4. data/README.md +293 -0
  5. data/Rakefile +26 -0
  6. data/basecamp-sdk.gemspec +46 -0
  7. data/lib/basecamp/auth_strategy.rb +38 -0
  8. data/lib/basecamp/chain_hooks.rb +45 -0
  9. data/lib/basecamp/client.rb +428 -0
  10. data/lib/basecamp/config.rb +143 -0
  11. data/lib/basecamp/errors.rb +289 -0
  12. data/lib/basecamp/generated/metadata.json +2281 -0
  13. data/lib/basecamp/generated/services/attachments_service.rb +24 -0
  14. data/lib/basecamp/generated/services/boosts_service.rb +70 -0
  15. data/lib/basecamp/generated/services/campfires_service.rb +122 -0
  16. data/lib/basecamp/generated/services/card_columns_service.rb +103 -0
  17. data/lib/basecamp/generated/services/card_steps_service.rb +57 -0
  18. data/lib/basecamp/generated/services/card_tables_service.rb +20 -0
  19. data/lib/basecamp/generated/services/cards_service.rb +66 -0
  20. data/lib/basecamp/generated/services/checkins_service.rb +157 -0
  21. data/lib/basecamp/generated/services/client_approvals_service.rb +28 -0
  22. data/lib/basecamp/generated/services/client_correspondences_service.rb +28 -0
  23. data/lib/basecamp/generated/services/client_replies_service.rb +30 -0
  24. data/lib/basecamp/generated/services/client_visibility_service.rb +21 -0
  25. data/lib/basecamp/generated/services/comments_service.rb +49 -0
  26. data/lib/basecamp/generated/services/documents_service.rb +52 -0
  27. data/lib/basecamp/generated/services/events_service.rb +20 -0
  28. data/lib/basecamp/generated/services/forwards_service.rb +67 -0
  29. data/lib/basecamp/generated/services/lineup_service.rb +44 -0
  30. data/lib/basecamp/generated/services/message_boards_service.rb +20 -0
  31. data/lib/basecamp/generated/services/message_types_service.rb +59 -0
  32. data/lib/basecamp/generated/services/messages_service.rb +75 -0
  33. data/lib/basecamp/generated/services/people_service.rb +73 -0
  34. data/lib/basecamp/generated/services/projects_service.rb +63 -0
  35. data/lib/basecamp/generated/services/recordings_service.rb +64 -0
  36. data/lib/basecamp/generated/services/reports_service.rb +56 -0
  37. data/lib/basecamp/generated/services/schedules_service.rb +92 -0
  38. data/lib/basecamp/generated/services/search_service.rb +31 -0
  39. data/lib/basecamp/generated/services/subscriptions_service.rb +50 -0
  40. data/lib/basecamp/generated/services/templates_service.rb +82 -0
  41. data/lib/basecamp/generated/services/timeline_service.rb +20 -0
  42. data/lib/basecamp/generated/services/timesheets_service.rb +81 -0
  43. data/lib/basecamp/generated/services/todolist_groups_service.rb +41 -0
  44. data/lib/basecamp/generated/services/todolists_service.rb +53 -0
  45. data/lib/basecamp/generated/services/todos_service.rb +106 -0
  46. data/lib/basecamp/generated/services/todosets_service.rb +20 -0
  47. data/lib/basecamp/generated/services/tools_service.rb +80 -0
  48. data/lib/basecamp/generated/services/uploads_service.rb +61 -0
  49. data/lib/basecamp/generated/services/vaults_service.rb +49 -0
  50. data/lib/basecamp/generated/services/webhooks_service.rb +63 -0
  51. data/lib/basecamp/generated/types.rb +3196 -0
  52. data/lib/basecamp/hooks.rb +70 -0
  53. data/lib/basecamp/http.rb +440 -0
  54. data/lib/basecamp/logger_hooks.rb +46 -0
  55. data/lib/basecamp/noop_hooks.rb +9 -0
  56. data/lib/basecamp/oauth/discovery.rb +123 -0
  57. data/lib/basecamp/oauth/errors.rb +35 -0
  58. data/lib/basecamp/oauth/exchange.rb +291 -0
  59. data/lib/basecamp/oauth/pkce.rb +68 -0
  60. data/lib/basecamp/oauth/types.rb +133 -0
  61. data/lib/basecamp/oauth.rb +56 -0
  62. data/lib/basecamp/oauth_token_provider.rb +108 -0
  63. data/lib/basecamp/operation_info.rb +17 -0
  64. data/lib/basecamp/request_info.rb +10 -0
  65. data/lib/basecamp/request_result.rb +14 -0
  66. data/lib/basecamp/security.rb +112 -0
  67. data/lib/basecamp/services/attachments_service.rb +33 -0
  68. data/lib/basecamp/services/authorization_service.rb +47 -0
  69. data/lib/basecamp/services/base_service.rb +146 -0
  70. data/lib/basecamp/services/campfires_service.rb +141 -0
  71. data/lib/basecamp/services/card_columns_service.rb +106 -0
  72. data/lib/basecamp/services/card_steps_service.rb +86 -0
  73. data/lib/basecamp/services/card_tables_service.rb +23 -0
  74. data/lib/basecamp/services/cards_service.rb +93 -0
  75. data/lib/basecamp/services/checkins_service.rb +127 -0
  76. data/lib/basecamp/services/client_approvals_service.rb +33 -0
  77. data/lib/basecamp/services/client_correspondences_service.rb +33 -0
  78. data/lib/basecamp/services/client_replies_service.rb +35 -0
  79. data/lib/basecamp/services/comments_service.rb +63 -0
  80. data/lib/basecamp/services/documents_service.rb +74 -0
  81. data/lib/basecamp/services/events_service.rb +27 -0
  82. data/lib/basecamp/services/forwards_service.rb +80 -0
  83. data/lib/basecamp/services/lineup_service.rb +67 -0
  84. data/lib/basecamp/services/message_boards_service.rb +24 -0
  85. data/lib/basecamp/services/message_types_service.rb +79 -0
  86. data/lib/basecamp/services/messages_service.rb +133 -0
  87. data/lib/basecamp/services/people_service.rb +73 -0
  88. data/lib/basecamp/services/projects_service.rb +67 -0
  89. data/lib/basecamp/services/recordings_service.rb +127 -0
  90. data/lib/basecamp/services/reports_service.rb +80 -0
  91. data/lib/basecamp/services/schedules_service.rb +156 -0
  92. data/lib/basecamp/services/search_service.rb +36 -0
  93. data/lib/basecamp/services/subscriptions_service.rb +67 -0
  94. data/lib/basecamp/services/templates_service.rb +96 -0
  95. data/lib/basecamp/services/timeline_service.rb +62 -0
  96. data/lib/basecamp/services/timesheet_service.rb +68 -0
  97. data/lib/basecamp/services/todolist_groups_service.rb +100 -0
  98. data/lib/basecamp/services/todolists_service.rb +104 -0
  99. data/lib/basecamp/services/todos_service.rb +156 -0
  100. data/lib/basecamp/services/todosets_service.rb +23 -0
  101. data/lib/basecamp/services/tools_service.rb +89 -0
  102. data/lib/basecamp/services/uploads_service.rb +84 -0
  103. data/lib/basecamp/services/vaults_service.rb +84 -0
  104. data/lib/basecamp/services/webhooks_service.rb +88 -0
  105. data/lib/basecamp/static_token_provider.rb +24 -0
  106. data/lib/basecamp/token_provider.rb +42 -0
  107. data/lib/basecamp/version.rb +6 -0
  108. data/lib/basecamp/webhooks/event.rb +52 -0
  109. data/lib/basecamp/webhooks/rack_middleware.rb +49 -0
  110. data/lib/basecamp/webhooks/receiver.rb +161 -0
  111. data/lib/basecamp/webhooks/verify.rb +36 -0
  112. data/lib/basecamp.rb +107 -0
  113. data/scripts/generate-metadata.rb +106 -0
  114. data/scripts/generate-services.rb +778 -0
  115. data/scripts/generate-types.rb +191 -0
  116. metadata +316 -0
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for schedule (calendar) operations.
6
+ #
7
+ # Schedules are calendars within projects that contain schedule entries (events).
8
+ # Each project has one schedule that can optionally show todo due dates.
9
+ #
10
+ # @example Get a schedule
11
+ # schedule = account.schedules.get(project_id: 123, schedule_id: 456)
12
+ #
13
+ # @example List entries
14
+ # account.schedules.list_entries(project_id: 123, schedule_id: 456).each do |entry|
15
+ # puts "#{entry["summary"]} - #{entry["starts_at"]}"
16
+ # end
17
+ #
18
+ # @example Create an entry
19
+ # entry = account.schedules.create_entry(
20
+ # project_id: 123,
21
+ # schedule_id: 456,
22
+ # summary: "Team Meeting",
23
+ # starts_at: "2024-12-15T09:00:00Z",
24
+ # ends_at: "2024-12-15T10:00:00Z"
25
+ # )
26
+ class SchedulesService < BaseService
27
+ # Gets a specific schedule.
28
+ #
29
+ # @param project_id [Integer, String] project (bucket) ID
30
+ # @param schedule_id [Integer, String] schedule ID
31
+ # @return [Hash] schedule data
32
+ def get(project_id:, schedule_id:)
33
+ http_get(bucket_path(project_id, "/schedules/#{schedule_id}")).json
34
+ end
35
+
36
+ # Lists all entries on a schedule.
37
+ #
38
+ # @param project_id [Integer, String] project (bucket) ID
39
+ # @param schedule_id [Integer, String] schedule ID
40
+ # @return [Enumerator<Hash>] schedule entries
41
+ def list_entries(project_id:, schedule_id:)
42
+ paginate(bucket_path(project_id, "/schedules/#{schedule_id}/entries.json"))
43
+ end
44
+
45
+ # Gets a specific schedule entry.
46
+ #
47
+ # @param project_id [Integer, String] project (bucket) ID
48
+ # @param entry_id [Integer, String] schedule entry ID
49
+ # @return [Hash] schedule entry data
50
+ def get_entry(project_id:, entry_id:)
51
+ http_get(bucket_path(project_id, "/schedule_entries/#{entry_id}")).json
52
+ end
53
+
54
+ # Creates a new entry on a schedule.
55
+ #
56
+ # @param project_id [Integer, String] project (bucket) ID
57
+ # @param schedule_id [Integer, String] schedule ID
58
+ # @param summary [String] event title
59
+ # @param starts_at [String] start time in RFC3339 format
60
+ # @param ends_at [String] end time in RFC3339 format
61
+ # @param description [String, nil] event description in HTML
62
+ # @param participant_ids [Array<Integer>, nil] person IDs to assign
63
+ # @param all_day [Boolean, nil] whether this is an all-day event
64
+ # @param notify [Boolean, nil] whether to notify participants
65
+ # @return [Hash] created entry
66
+ def create_entry(
67
+ project_id:,
68
+ schedule_id:,
69
+ summary:,
70
+ starts_at:,
71
+ ends_at:,
72
+ description: nil,
73
+ participant_ids: nil,
74
+ all_day: nil,
75
+ notify: nil
76
+ )
77
+ body = compact_params(
78
+ summary: summary,
79
+ starts_at: starts_at,
80
+ ends_at: ends_at,
81
+ description: description,
82
+ participant_ids: participant_ids,
83
+ all_day: all_day,
84
+ notify: notify
85
+ )
86
+ http_post(bucket_path(project_id, "/schedules/#{schedule_id}/entries.json"), body: body).json
87
+ end
88
+
89
+ # Updates an existing schedule entry.
90
+ #
91
+ # @param project_id [Integer, String] project (bucket) ID
92
+ # @param entry_id [Integer, String] schedule entry ID
93
+ # @param summary [String, nil] new title
94
+ # @param starts_at [String, nil] new start time in RFC3339 format
95
+ # @param ends_at [String, nil] new end time in RFC3339 format
96
+ # @param description [String, nil] new description in HTML
97
+ # @param participant_ids [Array<Integer>, nil] new participant IDs
98
+ # @param all_day [Boolean, nil] whether this is an all-day event
99
+ # @param notify [Boolean, nil] whether to notify participants
100
+ # @return [Hash] updated entry
101
+ def update_entry(
102
+ project_id:,
103
+ entry_id:,
104
+ summary: nil,
105
+ starts_at: nil,
106
+ ends_at: nil,
107
+ description: nil,
108
+ participant_ids: nil,
109
+ all_day: nil,
110
+ notify: nil
111
+ )
112
+ body = compact_params(
113
+ summary: summary,
114
+ starts_at: starts_at,
115
+ ends_at: ends_at,
116
+ description: description,
117
+ participant_ids: participant_ids,
118
+ all_day: all_day,
119
+ notify: notify
120
+ )
121
+ http_put(bucket_path(project_id, "/schedule_entries/#{entry_id}"), body: body).json
122
+ end
123
+
124
+ # Gets a specific occurrence of a recurring schedule entry.
125
+ #
126
+ # @param project_id [Integer, String] project (bucket) ID
127
+ # @param entry_id [Integer, String] schedule entry ID
128
+ # @param date [String] occurrence date in YYYY-MM-DD format
129
+ # @return [Hash] schedule entry occurrence
130
+ def get_entry_occurrence(project_id:, entry_id:, date:)
131
+ http_get(bucket_path(project_id, "/schedule_entries/#{entry_id}/occurrences/#{date}")).json
132
+ end
133
+
134
+ # Updates schedule settings.
135
+ #
136
+ # @param project_id [Integer, String] project (bucket) ID
137
+ # @param schedule_id [Integer, String] schedule ID
138
+ # @param include_due_assignments [Boolean] whether to show todo due dates
139
+ # @return [Hash] updated schedule
140
+ def update_settings(project_id:, schedule_id:, include_due_assignments:)
141
+ body = { include_due_assignments: include_due_assignments }
142
+ http_put(bucket_path(project_id, "/schedules/#{schedule_id}"), body: body).json
143
+ end
144
+
145
+ # Moves a schedule entry to the trash.
146
+ #
147
+ # @param project_id [Integer, String] project (bucket) ID
148
+ # @param entry_id [Integer, String] schedule entry ID
149
+ # @return [void]
150
+ def trash_entry(project_id:, entry_id:)
151
+ http_put(bucket_path(project_id, "/recordings/#{entry_id}/status/trashed.json"))
152
+ nil
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for search operations.
6
+ #
7
+ # Provides full-text search across all content in your Basecamp account.
8
+ #
9
+ # @example Search for content
10
+ # results = account.search.search(query: "quarterly report")
11
+ # results.each { |r| puts r["title"] }
12
+ #
13
+ # @example Get search metadata
14
+ # metadata = account.search.metadata
15
+ # puts "Available projects: #{metadata["projects"].length}"
16
+ class SearchService < BaseService
17
+ # Searches for content across the account.
18
+ #
19
+ # @param query [String] the search query string
20
+ # @param sort [String, nil] sort order ("created_at" or "updated_at")
21
+ # @return [Enumerator<Hash>] search results
22
+ def search(query:, sort: nil)
23
+ params = compact_params(query: query, sort: sort)
24
+ paginate("/search.json", params: params)
25
+ end
26
+
27
+ # Returns metadata about available search scopes.
28
+ # This includes the list of projects available for filtering.
29
+ #
30
+ # @return [Hash] search metadata with available filter options
31
+ def metadata
32
+ http_get("/searches/metadata.json").json
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for subscription operations.
6
+ #
7
+ # Subscriptions control who receives notifications for a specific recording
8
+ # (like a todo, message, or comment). Users can subscribe or unsubscribe
9
+ # themselves, and you can batch update subscriptions for multiple users.
10
+ #
11
+ # @example Get subscription info
12
+ # subscription = account.subscriptions.get(project_id: 123, recording_id: 456)
13
+ # puts "Subscribed: #{subscription["subscribed"]}, Count: #{subscription["count"]}"
14
+ #
15
+ # @example Batch update subscriptions
16
+ # account.subscriptions.update(
17
+ # project_id: 123,
18
+ # recording_id: 456,
19
+ # subscriptions: [user_id_1, user_id_2],
20
+ # unsubscriptions: [user_id_3]
21
+ # )
22
+ class SubscriptionsService < BaseService
23
+ # Gets the subscription information for a recording.
24
+ #
25
+ # @param project_id [Integer, String] project (bucket) ID
26
+ # @param recording_id [Integer, String] recording ID
27
+ # @return [Hash] subscription info with subscribed, count, subscribers
28
+ def get(project_id:, recording_id:)
29
+ http_get(bucket_path(project_id, "/recordings/#{recording_id}/subscription.json")).json
30
+ end
31
+
32
+ # Subscribes the current user to the recording.
33
+ #
34
+ # @param project_id [Integer, String] project (bucket) ID
35
+ # @param recording_id [Integer, String] recording ID
36
+ # @return [Hash] updated subscription information
37
+ def subscribe(project_id:, recording_id:)
38
+ http_post(bucket_path(project_id, "/recordings/#{recording_id}/subscription.json")).json
39
+ end
40
+
41
+ # Unsubscribes the current user from the recording.
42
+ #
43
+ # @param project_id [Integer, String] project (bucket) ID
44
+ # @param recording_id [Integer, String] recording ID
45
+ # @return [void]
46
+ def unsubscribe(project_id:, recording_id:)
47
+ http_delete(bucket_path(project_id, "/recordings/#{recording_id}/subscription.json"))
48
+ nil
49
+ end
50
+
51
+ # Batch modifies subscriptions by adding or removing specific users.
52
+ #
53
+ # @param project_id [Integer, String] project (bucket) ID
54
+ # @param recording_id [Integer, String] recording ID
55
+ # @param subscriptions [Array<Integer>, nil] person IDs to subscribe
56
+ # @param unsubscriptions [Array<Integer>, nil] person IDs to unsubscribe
57
+ # @return [Hash] updated subscription information
58
+ def update(project_id:, recording_id:, subscriptions: nil, unsubscriptions: nil)
59
+ body = compact_params(
60
+ subscriptions: subscriptions,
61
+ unsubscriptions: unsubscriptions
62
+ )
63
+ http_put(bucket_path(project_id, "/recordings/#{recording_id}/subscription.json"), body: body).json
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for template operations.
6
+ #
7
+ # Templates allow you to create reusable project structures.
8
+ #
9
+ # @example List templates
10
+ # account.templates.list.each do |template|
11
+ # puts template["name"]
12
+ # end
13
+ #
14
+ # @example Create a project from template
15
+ # construction = account.templates.create_project(
16
+ # template_id: 123,
17
+ # name: "Q1 Planning"
18
+ # )
19
+ class TemplatesService < BaseService
20
+ # Lists all templates visible to the current user.
21
+ #
22
+ # @return [Enumerator<Hash>] templates
23
+ def list
24
+ paginate("/templates.json")
25
+ end
26
+
27
+ # Gets a template by ID.
28
+ #
29
+ # @param template_id [Integer, String] template ID
30
+ # @return [Hash] template data
31
+ def get(template_id:)
32
+ http_get("/templates/#{template_id}").json
33
+ end
34
+
35
+ # Creates a new template.
36
+ #
37
+ # @param name [String] template name
38
+ # @param description [String, nil] template description
39
+ # @return [Hash] created template
40
+ def create(name:, description: nil)
41
+ body = compact_params(
42
+ name: name,
43
+ description: description
44
+ )
45
+ http_post("/templates.json", body: body).json
46
+ end
47
+
48
+ # Updates an existing template.
49
+ #
50
+ # @param template_id [Integer, String] template ID
51
+ # @param name [String] new name
52
+ # @param description [String, nil] new description
53
+ # @return [Hash] updated template
54
+ def update(template_id:, name:, description: nil)
55
+ body = compact_params(
56
+ name: name,
57
+ description: description
58
+ )
59
+ http_put("/templates/#{template_id}", body: body).json
60
+ end
61
+
62
+ # Deletes a template.
63
+ #
64
+ # @param template_id [Integer, String] template ID
65
+ # @return [void]
66
+ def delete(template_id:)
67
+ http_delete("/templates/#{template_id}")
68
+ nil
69
+ end
70
+
71
+ # Creates a new project from a template.
72
+ # This operation is asynchronous. Use get_construction to check status.
73
+ #
74
+ # @param template_id [Integer, String] template ID
75
+ # @param name [String] project name
76
+ # @param description [String, nil] project description
77
+ # @return [Hash] project construction status
78
+ def create_project(template_id:, name:, description: nil)
79
+ body = compact_params(
80
+ name: name,
81
+ description: description
82
+ )
83
+ http_post("/templates/#{template_id}/project_constructions.json", body: body).json
84
+ end
85
+
86
+ # Gets the status of a project construction.
87
+ #
88
+ # @param template_id [Integer, String] template ID
89
+ # @param construction_id [Integer, String] construction ID
90
+ # @return [Hash] project construction status
91
+ def get_construction(template_id:, construction_id:)
92
+ http_get("/templates/#{template_id}/project_constructions/#{construction_id}").json
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for timeline and progress report operations.
6
+ #
7
+ # Provides access to activity feeds showing recent activity across the account,
8
+ # within specific projects, or for specific people.
9
+ #
10
+ # @example Get account-wide progress report
11
+ # account.timeline.progress.each { |event| puts event["action"] }
12
+ #
13
+ # @example Get project timeline
14
+ # account.timeline.project_timeline(project_id: 123).each { |event| puts event }
15
+ #
16
+ # @example Get a person's progress (returns hash with person info)
17
+ # result = account.timeline.person_progress(person_id: 456)
18
+ # puts result["person"]["name"]
19
+ # result["events"].each { |event| puts event["action"] }
20
+ class TimelineService < BaseService
21
+ # Returns the account-wide progress report.
22
+ # This shows recent activity across all projects.
23
+ # Results are paginated via Link header.
24
+ #
25
+ # @return [Enumerator<Hash>] timeline events
26
+ def progress
27
+ paginate_key("/reports/progress.json", key: "events")
28
+ end
29
+
30
+ # Returns the activity timeline for a specific project.
31
+ # Results are paginated via Link header.
32
+ #
33
+ # @param project_id [Integer, String] project (bucket) ID
34
+ # @return [Enumerator<Hash>] timeline events
35
+ def project_timeline(project_id:)
36
+ paginate_key(bucket_path(project_id, "/timeline.json"), key: "events")
37
+ end
38
+
39
+ # Returns the progress report for a specific person, including person metadata.
40
+ #
41
+ # @note Returns first page only to preserve person metadata.
42
+ # Use {#person_progress_events} for full paginated event stream.
43
+ # @param person_id [Integer, String] person ID
44
+ # @return [Hash] object with "person" and "events" keys
45
+ def person_progress(person_id:)
46
+ response = http_get("/reports/users/progress/#{person_id}")
47
+ response.json
48
+ end
49
+
50
+ # Returns all progress events for a specific person.
51
+ # Results are paginated via Link header.
52
+ #
53
+ # @note Does not include person metadata. Use {#person_progress} if you need
54
+ # the person object along with the first page of events.
55
+ # @param person_id [Integer, String] person ID
56
+ # @return [Enumerator<Hash>] timeline events
57
+ def person_progress_events(person_id:)
58
+ paginate_key("/reports/users/progress/#{person_id}", key: "events")
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for timesheet report operations.
6
+ #
7
+ # Provides access to timesheet reports at account, project, and recording levels.
8
+ # Supports filtering by date range and person.
9
+ #
10
+ # @example Get account-wide timesheet
11
+ # result = account.timesheet.report
12
+ # result["entries"].each { |e| puts e["hours"] }
13
+ #
14
+ # @example Get project timesheet with filters
15
+ # result = account.timesheet.project_report(
16
+ # project_id: 123,
17
+ # from: "2024-01-01",
18
+ # to: "2024-01-31"
19
+ # )
20
+ #
21
+ # @example Get timesheet for a specific todo
22
+ # result = account.timesheet.recording_report(
23
+ # project_id: 123,
24
+ # recording_id: 789
25
+ # )
26
+ class TimesheetService < BaseService
27
+ # Returns the account-wide timesheet report.
28
+ # This includes time entries across all projects in the account.
29
+ #
30
+ # @param from [String, nil] filter entries on or after this date (ISO 8601, e.g., "2024-01-01")
31
+ # @param to [String, nil] filter entries on or before this date (ISO 8601, e.g., "2024-01-31")
32
+ # @param person_id [Integer, nil] filter entries by a specific person
33
+ # @return [Hash] timesheet report object with "entries" array
34
+ def report(from: nil, to: nil, person_id: nil)
35
+ params = compact_params(from: from, to: to, person_id: person_id)
36
+ response = http_get("/reports/timesheet.json", params: params)
37
+ response.json
38
+ end
39
+
40
+ # Returns the timesheet report for a specific project.
41
+ #
42
+ # @param project_id [Integer, String] project (bucket) ID
43
+ # @param from [String, nil] filter entries on or after this date (ISO 8601)
44
+ # @param to [String, nil] filter entries on or before this date (ISO 8601)
45
+ # @param person_id [Integer, nil] filter entries by a specific person
46
+ # @return [Hash] timesheet report object with "entries" array
47
+ def project_report(project_id:, from: nil, to: nil, person_id: nil)
48
+ params = compact_params(from: from, to: to, person_id: person_id)
49
+ response = http_get(bucket_path(project_id, "/timesheet.json"), params: params)
50
+ response.json
51
+ end
52
+
53
+ # Returns the timesheet report for a specific recording within a project.
54
+ #
55
+ # @param project_id [Integer, String] project (bucket) ID
56
+ # @param recording_id [Integer, String] recording ID (e.g., a todo)
57
+ # @param from [String, nil] filter entries on or after this date (ISO 8601)
58
+ # @param to [String, nil] filter entries on or before this date (ISO 8601)
59
+ # @param person_id [Integer, nil] filter entries by a specific person
60
+ # @return [Hash] timesheet report object with "entries" array
61
+ def recording_report(project_id:, recording_id:, from: nil, to: nil, person_id: nil)
62
+ params = compact_params(from: from, to: to, person_id: person_id)
63
+ response = http_get(bucket_path(project_id, "/recordings/#{recording_id}/timesheet.json"), params: params)
64
+ response.json
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for todolist group operations.
6
+ #
7
+ # Todolist groups are organizational folders within a todolist that help
8
+ # organize related todos together.
9
+ #
10
+ # @example List groups in a todolist
11
+ # groups = account.todolist_groups.list(project_id: 123, todolist_id: 456)
12
+ #
13
+ # @example Create a new group
14
+ # group = account.todolist_groups.create(
15
+ # project_id: 123,
16
+ # todolist_id: 456,
17
+ # name: "Phase 1"
18
+ # )
19
+ #
20
+ # @example Update a group name
21
+ # group = account.todolist_groups.update(
22
+ # project_id: 123,
23
+ # group_id: 789,
24
+ # name: "Phase 1 - Complete"
25
+ # )
26
+ class TodolistGroupsService < BaseService
27
+ # Lists all groups in a todolist.
28
+ #
29
+ # @param project_id [Integer, String] project (bucket) ID
30
+ # @param todolist_id [Integer, String] todolist ID
31
+ # @return [Enumerator<Hash>] todolist groups
32
+ def list(project_id:, todolist_id:)
33
+ paginate(bucket_path(project_id, "/todolists/#{todolist_id}/groups.json"))
34
+ end
35
+
36
+ # Gets a specific todolist group.
37
+ #
38
+ # Note: Groups are fetched via the todolists endpoint (polymorphic).
39
+ #
40
+ # @param project_id [Integer, String] project (bucket) ID
41
+ # @param group_id [Integer, String] group ID
42
+ # @return [Hash] group data
43
+ def get(project_id:, group_id:)
44
+ http_get(bucket_path(project_id, "/todolists/#{group_id}.json")).json
45
+ end
46
+
47
+ # Creates a new group in a todolist.
48
+ #
49
+ # @param project_id [Integer, String] project (bucket) ID
50
+ # @param todolist_id [Integer, String] todolist ID
51
+ # @param name [String] group name (required)
52
+ # @return [Hash] created group
53
+ # @raise [ArgumentError] if name is empty
54
+ def create(project_id:, todolist_id:, name:)
55
+ raise ArgumentError, "group name is required" if name.nil? || name.empty?
56
+
57
+ body = { name: name }
58
+ http_post(bucket_path(project_id, "/todolists/#{todolist_id}/groups.json"), body: body).json
59
+ end
60
+
61
+ # Updates an existing todolist group.
62
+ #
63
+ # Note: Groups are updated via the todolists endpoint (polymorphic).
64
+ #
65
+ # @param project_id [Integer, String] project (bucket) ID
66
+ # @param group_id [Integer, String] group ID
67
+ # @param name [String, nil] new group name
68
+ # @return [Hash] updated group
69
+ def update(project_id:, group_id:, name: nil)
70
+ body = compact_params(name: name)
71
+ http_put(bucket_path(project_id, "/todolists/#{group_id}.json"), body: body).json
72
+ end
73
+
74
+ # Repositions a group within its todolist.
75
+ #
76
+ # @param project_id [Integer, String] project (bucket) ID
77
+ # @param group_id [Integer, String] group ID
78
+ # @param position [Integer] new position (1-based, 1 = first position)
79
+ # @return [void]
80
+ # @raise [ArgumentError] if position is less than 1
81
+ def reposition(project_id:, group_id:, position:)
82
+ raise ArgumentError, "position must be at least 1" if position < 1
83
+
84
+ http_put(bucket_path(project_id, "/todolists/#{group_id}/position.json"), body: { position: position })
85
+ nil
86
+ end
87
+
88
+ # Moves a group to the trash.
89
+ # Trashed groups can be recovered from the trash.
90
+ #
91
+ # @param project_id [Integer, String] project (bucket) ID
92
+ # @param group_id [Integer, String] group ID
93
+ # @return [void]
94
+ def trash(project_id:, group_id:)
95
+ http_put(bucket_path(project_id, "/recordings/#{group_id}/status/trashed.json"))
96
+ nil
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for todolist operations.
6
+ #
7
+ # Todolists are collections of todos within a todoset. A project has one
8
+ # todoset which contains multiple todolists.
9
+ #
10
+ # @example List todolists
11
+ # account.todolists.list(project_id: 123, todoset_id: 456).each do |list|
12
+ # puts "#{list["name"]} - #{list["completed_ratio"]}"
13
+ # end
14
+ #
15
+ # @example Create a todolist
16
+ # todolist = account.todolists.create(
17
+ # project_id: 123,
18
+ # todoset_id: 456,
19
+ # name: "Launch Tasks"
20
+ # )
21
+ class TodolistsService < BaseService
22
+ # Lists all todolists in a todoset.
23
+ #
24
+ # @param project_id [Integer, String] project (bucket) ID
25
+ # @param todoset_id [Integer, String] todoset ID
26
+ # @param status [String, nil] filter by status ("archived", "trashed")
27
+ # @return [Enumerator<Hash>] todolists
28
+ def list(project_id:, todoset_id:, status: nil)
29
+ params = compact_params(status: status)
30
+ paginate(bucket_path(project_id, "/todosets/#{todoset_id}/todolists.json"), params: params)
31
+ end
32
+
33
+ # Gets a specific todolist.
34
+ #
35
+ # @param project_id [Integer, String] project (bucket) ID
36
+ # @param todolist_id [Integer, String] todolist ID
37
+ # @return [Hash] todolist data
38
+ def get(project_id:, todolist_id:)
39
+ http_get(bucket_path(project_id, "/todolists/#{todolist_id}")).json
40
+ end
41
+
42
+ # Creates a new todolist.
43
+ #
44
+ # @param project_id [Integer, String] project (bucket) ID
45
+ # @param todoset_id [Integer, String] todoset ID
46
+ # @param name [String] todolist name
47
+ # @param description [String, nil] todolist description in HTML
48
+ # @return [Hash] created todolist
49
+ def create(project_id:, todoset_id:, name:, description: nil)
50
+ body = compact_params(
51
+ name: name,
52
+ description: description
53
+ )
54
+ http_post(bucket_path(project_id, "/todosets/#{todoset_id}/todolists.json"), body: body).json
55
+ end
56
+
57
+ # Updates a todolist.
58
+ #
59
+ # @param project_id [Integer, String] project (bucket) ID
60
+ # @param todolist_id [Integer, String] todolist ID
61
+ # @param name [String, nil] new name
62
+ # @param description [String, nil] new description
63
+ # @return [Hash] updated todolist
64
+ def update(project_id:, todolist_id:, name: nil, description: nil)
65
+ body = compact_params(
66
+ name: name,
67
+ description: description
68
+ )
69
+ http_put(bucket_path(project_id, "/todolists/#{todolist_id}"), body: body).json
70
+ end
71
+
72
+ # Lists all todolist groups within a todolist.
73
+ #
74
+ # @param project_id [Integer, String] project (bucket) ID
75
+ # @param todolist_id [Integer, String] todolist ID
76
+ # @return [Enumerator<Hash>] todolist groups
77
+ def list_groups(project_id:, todolist_id:)
78
+ paginate(bucket_path(project_id, "/todolists/#{todolist_id}/groups.json"))
79
+ end
80
+
81
+ # Creates a new todolist group.
82
+ #
83
+ # @param project_id [Integer, String] project (bucket) ID
84
+ # @param todolist_id [Integer, String] todolist ID
85
+ # @param name [String] group name
86
+ # @return [Hash] created group
87
+ def create_group(project_id:, todolist_id:, name:)
88
+ body = { name: name }
89
+ http_post(bucket_path(project_id, "/todolists/#{todolist_id}/groups.json"), body: body).json
90
+ end
91
+
92
+ # Repositions a todolist group.
93
+ #
94
+ # @param project_id [Integer, String] project (bucket) ID
95
+ # @param todolist_id [Integer, String] todolist ID (the group's ID)
96
+ # @param position [Integer] new position (1-based)
97
+ # @return [void]
98
+ def reposition_group(project_id:, todolist_id:, position:)
99
+ http_put(bucket_path(project_id, "/todolists/#{todolist_id}/position.json"), body: { position: position })
100
+ nil
101
+ end
102
+ end
103
+ end
104
+ end