timet 1.5.3 → 1.5.5
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/.rubocop.yml +10 -1
- data/CHANGELOG.md +48 -1
- data/lib/timet/application.rb +7 -7
- data/lib/timet/application_helper.rb +2 -2
- data/lib/timet/database.rb +1 -3
- data/lib/timet/database_sync_helper.rb +4 -206
- data/lib/timet/database_syncer.rb +214 -0
- data/lib/timet/s3_supabase.rb +12 -5
- data/lib/timet/table.rb +25 -9
- data/lib/timet/time_block_chart.rb +70 -21
- data/lib/timet/time_report.rb +50 -25
- data/lib/timet/validation_edit_helper.rb +6 -6
- data/lib/timet/version.rb +2 -2
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bc6553610acca8f30eb067accd9a8ff09756ea7c0fdb231dac896210018e0db1
|
4
|
+
data.tar.gz: 41e673fa16e1a7ea7d7c7c3f75a7131c7add0fa4ea297ae3044ca9ddbbd1b4f3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '01793d4487cd53b78a2d35721a3e6d91ff29a8c98976e9e861bfc599e49dfbf89939bec66da4fd986f8b8f99c4cf2001ac859274859185aaf4a89f9f56bae7a3'
|
7
|
+
data.tar.gz: c0f27b5b4656c07a16e861544daf610b94d397a5b245e89d65850c23e24f1a5318042b6871430cd2c0341c0bdb3f27b4163781bbf024680157cd68889202459c
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,51 @@
|
|
1
|
-
## [
|
1
|
+
## [1.5.5] - 2025-02-26
|
2
|
+
|
3
|
+
**Improvements:**
|
4
|
+
|
5
|
+
- Refactored `DatabaseSyncer` specs to improve clarity, maintainability, and reduce redundancy.
|
6
|
+
- Removed `#process_existing_item` as its logic was redundant.
|
7
|
+
- Improved `#remote_wins?` tests with `let` blocks and better descriptions.
|
8
|
+
- Split multi-expectation tests into smaller, focused tests.
|
9
|
+
- Improved test descriptions for accuracy and clarity.
|
10
|
+
- Reorganized tests into more logical `describe` blocks.
|
11
|
+
- Moved S3 download tests to the `Timet::S3Supabase` `describe` block.
|
12
|
+
- Improved CSV export tests with extracted common data, centralized database setup, and a helper method.
|
13
|
+
- Improved message expectations in `#export_report` using `class_spy` and `have_received`.
|
14
|
+
- Improved test readability in `ApplicationHelper` with `let` blocks and separate `it` blocks.
|
15
|
+
- Refactored `DatabaseSyncer` specs with `let` blocks, focused examples, and explicit `expect` assertions.
|
16
|
+
- Prefer `have_received` for setting message expectations.
|
17
|
+
- Improved granularity in specs with smaller, focused examples and shared test data.
|
18
|
+
- Updated gem dependencies to latest versions.
|
19
|
+
- Overall code cleanup and improved test structure.
|
20
|
+
- Update the way rubocop plugins are defined in the config.
|
21
|
+
- Removed unnecessary comments.
|
22
|
+
|
23
|
+
**Bug Fixes:**
|
24
|
+
|
25
|
+
- Fixed a bug in the time field update logic in `validation_edit_helper`, specifically regarding the end time.
|
26
|
+
|
27
|
+
## [1.5.4] - 2025-02-11
|
28
|
+
|
29
|
+
**Improvements:**
|
30
|
+
|
31
|
+
- Added `.env` file creation in CI workflow for testing environment variables.
|
32
|
+
- Updated Code Climate coverage reporting to use `simplecov-lcov` for LCOV format compatibility.
|
33
|
+
- Refactored validation error message tests in `ValidationEditHelper` for clarity and maintainability.
|
34
|
+
- Simplified environment variable validation in `S3Supabase` by introducing a helper method `check_env_var`.
|
35
|
+
- Improved integration tests for `play_sound_and_notify` with better stubbing and platform-specific behavior checks.
|
36
|
+
- Added comprehensive tests for `#process_existing_item`, `#update_item_from_hash`, and `#insert_item_from_hash` in `DatabaseSyncer`.
|
37
|
+
- Enhanced database synchronization logic and moved `ITEM_FIELDS` to `DatabaseSyncer` for better organization.
|
38
|
+
- Added `.format_time_string` method to `TimeHelper` with tests for various input formats.
|
39
|
+
- Updated AWS SDK dependencies and improved database sync logging.
|
40
|
+
- Consolidated integration tests and improved readability.
|
41
|
+
- Improved error handling and added tests for `S3Supabase`.
|
42
|
+
|
43
|
+
**Bug Fixes:**
|
44
|
+
|
45
|
+
- Fixed environment variable validation in `S3Supabase` to handle `nil` values.
|
46
|
+
- Resolved issues with database synchronization logic in `DatabaseSyncer`.
|
47
|
+
- Fixed test setup and cleanup in `S3Supabase` and `DatabaseSyncer` specs.
|
48
|
+
- Addressed edge cases in `#process_and_update_time_field` and `#valid_time_value?`.
|
2
49
|
|
3
50
|
## [1.5.3] - 2025-01-02
|
4
51
|
|
data/lib/timet/application.rb
CHANGED
@@ -52,15 +52,14 @@ module Timet
|
|
52
52
|
def initialize(*args)
|
53
53
|
super
|
54
54
|
|
55
|
-
# Initialize database without validation in test environment
|
56
55
|
if defined?(RSpec)
|
57
56
|
@db = Database.new
|
58
57
|
else
|
59
|
-
command_name = args
|
58
|
+
command_name = args.dig(2, :current_command, :name)
|
60
59
|
if VALID_ARGUMENTS.include?(command_name)
|
61
60
|
@db = Database.new
|
62
61
|
else
|
63
|
-
|
62
|
+
warn 'Invalid arguments provided. Please check your input.'
|
64
63
|
exit(1)
|
65
64
|
end
|
66
65
|
end
|
@@ -110,6 +109,7 @@ module Timet
|
|
110
109
|
|
111
110
|
desc 'stop', 'Stop time tracking'
|
112
111
|
# Stops the current tracking session if there is one in progress.
|
112
|
+
# After stopping the tracking session, it displays a summary of the tracked time.
|
113
113
|
#
|
114
114
|
# @return [void] This method does not return a value; it performs side effects such as updating the tracking item
|
115
115
|
# and generating a summary.
|
@@ -120,15 +120,14 @@ module Timet
|
|
120
120
|
# @note The method checks if the last tracking item is in progress by calling `@db.item_status`.
|
121
121
|
# @note If the last item is in progress, it fetches the last item's ID using `@db.fetch_last_id` and updates it
|
122
122
|
# with the current timestamp.
|
123
|
-
# @note The method
|
124
|
-
|
125
|
-
def stop(display = nil)
|
123
|
+
# @note The method always generates a summary after stopping the tracking session.
|
124
|
+
def stop
|
126
125
|
return unless @db.item_status == :in_progress
|
127
126
|
|
128
127
|
last_id = @db.fetch_last_id
|
129
128
|
@db.update_item(last_id, 'end', TimeHelper.current_timestamp)
|
130
129
|
|
131
|
-
summary
|
130
|
+
summary
|
132
131
|
end
|
133
132
|
|
134
133
|
desc 'resume (r) [id]', 'Resume last task (id is an optional parameter) => tt resume'
|
@@ -306,6 +305,7 @@ module Timet
|
|
306
305
|
desc 'sync', 'Sync local db with supabase external db'
|
307
306
|
def sync
|
308
307
|
puts 'Syncing database with remote storage...'
|
308
|
+
puts 'Sync method called'
|
309
309
|
DatabaseSyncHelper.sync(@db, BUCKET)
|
310
310
|
end
|
311
311
|
end
|
@@ -111,7 +111,7 @@ module Timet
|
|
111
111
|
def run_linux_session(time, tag)
|
112
112
|
notification_command = "notify-send --icon=clock '#{show_message(tag)}'"
|
113
113
|
command = "sleep #{time} && tput bel && tt stop 0 && #{notification_command} &"
|
114
|
-
pid = spawn(command)
|
114
|
+
pid = Kernel.spawn(command)
|
115
115
|
Process.detach(pid)
|
116
116
|
end
|
117
117
|
|
@@ -123,7 +123,7 @@ module Timet
|
|
123
123
|
def run_mac_session(time, tag)
|
124
124
|
notification_command = "osascript -e 'display notification \"#{show_message(tag)}\"'"
|
125
125
|
command = "sleep #{time} && afplay /System/Library/Sounds/Basso.aiff && tt stop 0 && #{notification_command} &"
|
126
|
-
pid = spawn(command)
|
126
|
+
pid = Kernel.spawn(command)
|
127
127
|
Process.detach(pid)
|
128
128
|
end
|
129
129
|
|
data/lib/timet/database.rb
CHANGED
@@ -285,8 +285,6 @@ module Timet
|
|
285
285
|
:complete
|
286
286
|
end
|
287
287
|
|
288
|
-
private
|
289
|
-
|
290
288
|
# Moves the old database file to the new location if it exists.
|
291
289
|
#
|
292
290
|
# @param database_path [String] The path to the new SQLite database file.
|
@@ -314,7 +312,7 @@ module Timet
|
|
314
312
|
# @raise [StandardError] If there is an issue executing the SQL queries, an error may be raised.
|
315
313
|
#
|
316
314
|
def update_time_columns
|
317
|
-
result = execute_sql('SELECT * FROM items
|
315
|
+
result = execute_sql('SELECT * FROM items WHERE updated_at IS NULL OR created_at IS NULL')
|
318
316
|
result.each do |item|
|
319
317
|
id = item[0]
|
320
318
|
end_time = item[2]
|
@@ -2,13 +2,13 @@
|
|
2
2
|
|
3
3
|
require 'tempfile'
|
4
4
|
require 'digest'
|
5
|
+
require_relative 'database_syncer'
|
5
6
|
|
6
7
|
module Timet
|
7
8
|
# Helper module for database synchronization operations
|
8
9
|
# Provides methods for comparing and syncing local and remote databases
|
9
10
|
module DatabaseSyncHelper
|
10
|
-
|
11
|
-
ITEM_FIELDS = %w[start end tag notes pomodoro updated_at created_at deleted].freeze
|
11
|
+
extend DatabaseSyncer
|
12
12
|
|
13
13
|
# Main entry point for database synchronization
|
14
14
|
#
|
@@ -57,6 +57,8 @@ module Timet
|
|
57
57
|
# @note This method ensures proper resource cleanup by using ensure block
|
58
58
|
def self.with_temp_file
|
59
59
|
temp_file = Tempfile.new('remote_db')
|
60
|
+
raise 'Temporary file path is nil' unless temp_file.path
|
61
|
+
|
60
62
|
yield temp_file
|
61
63
|
ensure
|
62
64
|
temp_file.close
|
@@ -74,209 +76,5 @@ module Timet
|
|
74
76
|
local_md5 = Digest::MD5.file(local_path).hexdigest
|
75
77
|
remote_md5 == local_md5
|
76
78
|
end
|
77
|
-
|
78
|
-
# Handles the synchronization process when differences are detected between databases
|
79
|
-
#
|
80
|
-
# @param local_db [SQLite3::Database] The local database connection
|
81
|
-
# @param remote_storage [S3Supabase] The remote storage client for cloud operations
|
82
|
-
# @param bucket [String] The S3 bucket name
|
83
|
-
# @param local_db_path [String] Path to the local database file
|
84
|
-
# @param remote_path [String] Path to the downloaded remote database file
|
85
|
-
# @return [void]
|
86
|
-
# @note This method attempts to sync the databases and handles any errors that occur during the process
|
87
|
-
def self.handle_database_differences(*args)
|
88
|
-
local_db, remote_storage, bucket, local_db_path, remote_path = args
|
89
|
-
puts 'Differences detected between local and remote databases'
|
90
|
-
begin
|
91
|
-
sync_with_remote_database(local_db, remote_path, remote_storage, bucket, local_db_path)
|
92
|
-
rescue SQLite3::Exception => e
|
93
|
-
handle_sync_error(e, remote_storage, bucket, local_db_path)
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
# Performs the actual database synchronization by setting up connections and syncing data
|
98
|
-
#
|
99
|
-
# @param local_db [SQLite3::Database] The local database connection
|
100
|
-
# @param remote_path [String] Path to the remote database file
|
101
|
-
# @param remote_storage [S3Supabase] The remote storage client for cloud operations
|
102
|
-
# @param bucket [String] The S3 bucket name
|
103
|
-
# @param local_db_path [String] Path to the local database file
|
104
|
-
# @return [void]
|
105
|
-
# @note Configures both databases to return results as hashes for consistent data handling
|
106
|
-
def self.sync_with_remote_database(*args)
|
107
|
-
local_db, remote_path, remote_storage, bucket, local_db_path = args
|
108
|
-
db_remote = open_remote_database(remote_path)
|
109
|
-
db_remote.results_as_hash = true
|
110
|
-
local_db.instance_variable_get(:@db).results_as_hash = true
|
111
|
-
sync_databases(local_db, db_remote, remote_storage, bucket, local_db_path)
|
112
|
-
end
|
113
|
-
|
114
|
-
# Opens and validates a connection to the remote database
|
115
|
-
#
|
116
|
-
# @param remote_path [String] Path to the remote database file
|
117
|
-
# @return [SQLite3::Database] The initialized database connection
|
118
|
-
# @raise [RuntimeError] If the database connection cannot be established
|
119
|
-
# @note Validates that the database connection is properly initialized
|
120
|
-
def self.open_remote_database(remote_path)
|
121
|
-
db_remote = SQLite3::Database.new(remote_path)
|
122
|
-
raise 'Failed to initialize remote database' unless db_remote
|
123
|
-
|
124
|
-
db_remote
|
125
|
-
end
|
126
|
-
|
127
|
-
# Handles errors that occur during database synchronization
|
128
|
-
#
|
129
|
-
# @param error [SQLite3::Exception] The error that occurred during sync
|
130
|
-
# @param remote_storage [S3Supabase] The remote storage client for cloud operations
|
131
|
-
# @param bucket [String] The S3 bucket name
|
132
|
-
# @param local_db_path [String] Path to the local database file
|
133
|
-
# @return [void]
|
134
|
-
# @note When sync fails, this method falls back to uploading the local database
|
135
|
-
def self.handle_sync_error(error, remote_storage, bucket, local_db_path)
|
136
|
-
puts "Error opening remote database: #{error.message}"
|
137
|
-
puts 'Uploading local database to replace corrupted remote database'
|
138
|
-
remote_storage.upload_file(bucket, local_db_path, 'timet.db')
|
139
|
-
end
|
140
|
-
|
141
|
-
# Converts database items to a hash indexed by ID
|
142
|
-
#
|
143
|
-
# @param items [Array<Hash>] Array of database items
|
144
|
-
# @return [Hash] Items indexed by ID
|
145
|
-
def self.items_to_hash(items)
|
146
|
-
items.to_h { |item| [item['id'], item] }
|
147
|
-
end
|
148
|
-
|
149
|
-
# Determines if remote item should take precedence
|
150
|
-
#
|
151
|
-
# @param remote_item [Hash] Remote database item
|
152
|
-
# @param remote_time [Integer] Remote item timestamp
|
153
|
-
# @param local_time [Integer] Local item timestamp
|
154
|
-
# @return [Boolean] true if remote item should take precedence
|
155
|
-
def self.remote_wins?(remote_item, remote_time, local_time)
|
156
|
-
remote_time > local_time && (remote_item['deleted'].to_i == 1 || remote_time > local_time)
|
157
|
-
end
|
158
|
-
|
159
|
-
# Formats item status message
|
160
|
-
#
|
161
|
-
# @param id [Integer] Item ID
|
162
|
-
# @param item [Hash] Database item
|
163
|
-
# @param source [String] Source of the item ('Remote' or 'Local')
|
164
|
-
# @return [String] Formatted status message
|
165
|
-
def self.format_status_message(id, item, source)
|
166
|
-
deleted = item['deleted'].to_i == 1 ? ' and deleted' : ''
|
167
|
-
"#{source} item #{id} is newer#{deleted} - #{source == 'Remote' ? 'updating local' : 'will be uploaded'}"
|
168
|
-
end
|
169
|
-
|
170
|
-
# Processes an item that exists in both databases
|
171
|
-
#
|
172
|
-
# @param id [Integer] Item ID
|
173
|
-
# @param local_item [Hash] Local database item
|
174
|
-
# @param remote_item [Hash] Remote database item
|
175
|
-
# @param local_db [SQLite3::Database] Local database connection
|
176
|
-
# @return [Symbol] :local_update if local was updated, :remote_update if remote needs update
|
177
|
-
def self.process_existing_item(*args)
|
178
|
-
id, local_item, remote_item, local_db = args
|
179
|
-
local_time = local_item['updated_at'].to_i
|
180
|
-
remote_time = remote_item['updated_at'].to_i
|
181
|
-
|
182
|
-
if remote_wins?(remote_item, remote_time, local_time)
|
183
|
-
puts format_status_message(id, remote_item, 'Remote')
|
184
|
-
update_item_from_hash(local_db, remote_item)
|
185
|
-
:local_update
|
186
|
-
elsif local_time > remote_time
|
187
|
-
puts format_status_message(id, local_item, 'Local')
|
188
|
-
:remote_update
|
189
|
-
end
|
190
|
-
end
|
191
|
-
|
192
|
-
# Processes items from both databases and syncs them
|
193
|
-
#
|
194
|
-
# @param local_db [SQLite3::Database] The local database connection
|
195
|
-
# @param remote_db [SQLite3::Database] The remote database connection
|
196
|
-
# @return [void]
|
197
|
-
def self.process_database_items(local_db, remote_db)
|
198
|
-
remote_items = remote_db.execute('SELECT * FROM items ORDER BY updated_at DESC')
|
199
|
-
local_items = local_db.execute_sql('SELECT * FROM items ORDER BY updated_at DESC')
|
200
|
-
|
201
|
-
sync_items_by_id(
|
202
|
-
local_db,
|
203
|
-
items_to_hash(local_items),
|
204
|
-
items_to_hash(remote_items)
|
205
|
-
)
|
206
|
-
end
|
207
|
-
|
208
|
-
# Syncs items between local and remote databases based on their IDs
|
209
|
-
#
|
210
|
-
# @param local_db [SQLite3::Database] The local database connection
|
211
|
-
# @param local_items_by_id [Hash] Local items indexed by ID
|
212
|
-
# @param remote_items_by_id [Hash] Remote items indexed by ID
|
213
|
-
# @return [void]
|
214
|
-
def self.sync_items_by_id(local_db, local_items_by_id, remote_items_by_id)
|
215
|
-
all_item_ids = (remote_items_by_id.keys + local_items_by_id.keys).uniq
|
216
|
-
|
217
|
-
all_item_ids.each do |id|
|
218
|
-
if !remote_items_by_id[id]
|
219
|
-
puts "Local item #{id} will be uploaded"
|
220
|
-
elsif !local_items_by_id[id]
|
221
|
-
puts "Adding remote item #{id} to local"
|
222
|
-
insert_item_from_hash(local_db, remote_items_by_id[id])
|
223
|
-
else
|
224
|
-
process_existing_item(id, local_items_by_id[id], remote_items_by_id[id], local_db)
|
225
|
-
end
|
226
|
-
end
|
227
|
-
end
|
228
|
-
|
229
|
-
# Synchronizes the local and remote databases by comparing and merging their items
|
230
|
-
#
|
231
|
-
# @param local_db [SQLite3::Database] The local database connection
|
232
|
-
# @param remote_db [SQLite3::Database] The remote database connection
|
233
|
-
# @param remote_storage [S3Supabase] The remote storage client for cloud operations
|
234
|
-
# @param bucket [String] The S3 bucket name
|
235
|
-
# @param local_db_path [String] Path to the local database file
|
236
|
-
# @return [void]
|
237
|
-
# @note This method orchestrates the entire database synchronization process
|
238
|
-
def self.sync_databases(*args)
|
239
|
-
local_db, remote_db, remote_storage, bucket, local_db_path = args
|
240
|
-
process_database_items(local_db, remote_db)
|
241
|
-
remote_storage.upload_file(bucket, local_db_path, 'timet.db')
|
242
|
-
puts 'Database sync completed'
|
243
|
-
end
|
244
|
-
|
245
|
-
# Gets the values array for database operations
|
246
|
-
#
|
247
|
-
# @param item [Hash] Hash containing item data
|
248
|
-
# @param include_id [Boolean] Whether to include ID at start (insert) or end (update)
|
249
|
-
# @return [Array] Array of values for database operation
|
250
|
-
def self.get_item_values(item, include_id_at_start: false)
|
251
|
-
values = ITEM_FIELDS.map { |field| item[field] }
|
252
|
-
include_id_at_start ? [item['id'], *values] : [*values, item['id']]
|
253
|
-
end
|
254
|
-
|
255
|
-
# Updates an existing item in the database with values from a hash
|
256
|
-
#
|
257
|
-
# @param db [SQLite3::Database] The database connection
|
258
|
-
# @param item [Hash] Hash containing item data
|
259
|
-
# @return [void]
|
260
|
-
def self.update_item_from_hash(db, item)
|
261
|
-
fields = "#{ITEM_FIELDS.join(' = ?, ')} = ?"
|
262
|
-
db.execute_sql(
|
263
|
-
"UPDATE items SET #{fields} WHERE id = ?",
|
264
|
-
get_item_values(item)
|
265
|
-
)
|
266
|
-
end
|
267
|
-
|
268
|
-
# Inserts a new item into the database from a hash
|
269
|
-
#
|
270
|
-
# @param db [SQLite3::Database] The database connection
|
271
|
-
# @param item [Hash] Hash containing item data
|
272
|
-
# @return [void]
|
273
|
-
def self.insert_item_from_hash(db, item)
|
274
|
-
fields = ['id', *ITEM_FIELDS].join(', ')
|
275
|
-
placeholders = Array.new(ITEM_FIELDS.length + 1, '?').join(', ')
|
276
|
-
db.execute_sql(
|
277
|
-
"INSERT INTO items (#{fields}) VALUES (#{placeholders})",
|
278
|
-
get_item_values(item, include_id_at_start: true)
|
279
|
-
)
|
280
|
-
end
|
281
79
|
end
|
282
80
|
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Timet
|
4
|
+
# Module responsible for synchronizing local and remote databases
|
5
|
+
module DatabaseSyncer
|
6
|
+
# Fields used in item operations
|
7
|
+
ITEM_FIELDS = %w[start end tag notes pomodoro updated_at created_at deleted].freeze
|
8
|
+
|
9
|
+
# Handles the synchronization process when differences are detected between databases
|
10
|
+
#
|
11
|
+
# @param local_db [SQLite3::Database] The local database connection
|
12
|
+
# @param remote_storage [S3Supabase] The remote storage client for cloud operations
|
13
|
+
# @param bucket [String] The S3 bucket name
|
14
|
+
# @param local_db_path [String] Path to the local database file
|
15
|
+
# @param remote_path [String] Path to the downloaded remote database file
|
16
|
+
# @return [void]
|
17
|
+
# @note This method attempts to sync the databases and handles any errors that occur during the process
|
18
|
+
def handle_database_differences(*args)
|
19
|
+
local_db, remote_storage, bucket, local_db_path, remote_path = args
|
20
|
+
puts 'Differences detected between local and remote databases'
|
21
|
+
begin
|
22
|
+
sync_with_remote_database(local_db, remote_path, remote_storage, bucket, local_db_path)
|
23
|
+
rescue SQLite3::Exception => e
|
24
|
+
handle_sync_error(e, remote_storage, bucket, local_db_path)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Handles errors that occur during database synchronization
|
29
|
+
#
|
30
|
+
# @param error [SQLite3::Exception] The error that occurred during sync
|
31
|
+
# @param remote_storage [S3Supabase] The remote storage client for cloud operations
|
32
|
+
# @param bucket [String] The S3 bucket name
|
33
|
+
# @param local_db_path [String] Path to the local database file
|
34
|
+
# @return [void]
|
35
|
+
# @note When sync fails, this method falls back to uploading the local database
|
36
|
+
def handle_sync_error(error, remote_storage, bucket, local_db_path)
|
37
|
+
puts "Error opening remote database: #{error.message}"
|
38
|
+
puts 'Uploading local database to replace corrupted remote database'
|
39
|
+
remote_storage.upload_file(bucket, local_db_path, 'timet.db')
|
40
|
+
end
|
41
|
+
|
42
|
+
# Performs the actual database synchronization by setting up connections and syncing data
|
43
|
+
#
|
44
|
+
# @param local_db [SQLite3::Database] The local database connection
|
45
|
+
# @param remote_path [String] Path to the remote database file
|
46
|
+
# @param remote_storage [S3Supabase] The remote storage client for cloud operations
|
47
|
+
# @param bucket [String] The S3 bucket name
|
48
|
+
# @param local_db_path [String] Path to the local database file
|
49
|
+
# @return [void]
|
50
|
+
# @note Configures both databases to return results as hashes for consistent data handling
|
51
|
+
def sync_with_remote_database(*args)
|
52
|
+
local_db, remote_path, remote_storage, bucket, local_db_path = args
|
53
|
+
db_remote = open_remote_database(remote_path)
|
54
|
+
db_remote.results_as_hash = true
|
55
|
+
local_db.instance_variable_get(:@db).results_as_hash = true
|
56
|
+
sync_databases(local_db, db_remote, remote_storage, bucket, local_db_path)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Opens and validates a connection to the remote database
|
60
|
+
#
|
61
|
+
# @param remote_path [String] Path to the remote database file
|
62
|
+
# @return [SQLite3::Database] The initialized database connection
|
63
|
+
# @raise [RuntimeError] If the database connection cannot be established
|
64
|
+
# @note Validates that the database connection is properly initialized
|
65
|
+
def open_remote_database(remote_path)
|
66
|
+
db_remote = SQLite3::Database.new(remote_path)
|
67
|
+
raise 'Failed to initialize remote database' unless db_remote
|
68
|
+
|
69
|
+
db_remote
|
70
|
+
end
|
71
|
+
|
72
|
+
# Synchronizes the local and remote databases by comparing and merging their items
|
73
|
+
#
|
74
|
+
# @param local_db [SQLite3::Database] The local database connection
|
75
|
+
# @param remote_db [SQLite3::Database] The remote database connection
|
76
|
+
# @param remote_storage [S3Supabase] The remote storage client for cloud operations
|
77
|
+
# @param bucket [String] The S3 bucket name
|
78
|
+
# @param local_db_path [String] Path to the local database file
|
79
|
+
# @return [void]
|
80
|
+
# @note This method orchestrates the entire database synchronization process
|
81
|
+
def sync_databases(*args)
|
82
|
+
local_db, remote_db, remote_storage, bucket, local_db_path = args
|
83
|
+
process_database_items(local_db, remote_db)
|
84
|
+
remote_storage.upload_file(bucket, local_db_path, 'timet.db')
|
85
|
+
puts 'Database sync completed'
|
86
|
+
end
|
87
|
+
|
88
|
+
# Processes items from both databases and syncs them
|
89
|
+
#
|
90
|
+
# @param local_db [SQLite3::Database] The local database connection
|
91
|
+
# @param remote_db [SQLite3::Database] The remote database connection
|
92
|
+
# @return [void]
|
93
|
+
def process_database_items(local_db, remote_db)
|
94
|
+
remote_items = remote_db.execute('SELECT * FROM items ORDER BY updated_at DESC')
|
95
|
+
local_items = local_db.execute_sql('SELECT * FROM items ORDER BY updated_at DESC')
|
96
|
+
|
97
|
+
sync_items_by_id(
|
98
|
+
local_db,
|
99
|
+
items_to_hash(local_items),
|
100
|
+
items_to_hash(remote_items)
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Syncs items between local and remote databases based on their IDs
|
105
|
+
#
|
106
|
+
# @param local_db [SQLite3::Database] The local database connection
|
107
|
+
# @param local_items_by_id [Hash] Local items indexed by ID
|
108
|
+
# @param remote_items_by_id [Hash] Remote items indexed by ID
|
109
|
+
# @return [void]
|
110
|
+
def sync_items_by_id(local_db, local_items_by_id, remote_items_by_id)
|
111
|
+
all_item_ids = (remote_items_by_id.keys + local_items_by_id.keys).uniq
|
112
|
+
|
113
|
+
all_item_ids.each do |id|
|
114
|
+
if !remote_items_by_id[id]
|
115
|
+
puts "Local item #{id} will be uploaded"
|
116
|
+
elsif !local_items_by_id[id]
|
117
|
+
puts "Adding remote item #{id} to local"
|
118
|
+
insert_item_from_hash(local_db, remote_items_by_id[id])
|
119
|
+
else
|
120
|
+
process_existing_item(id, local_items_by_id[id], remote_items_by_id[id], local_db)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Inserts a new item into the database from a hash
|
126
|
+
#
|
127
|
+
# @param db [SQLite3::Database] The database connection
|
128
|
+
# @param item [Hash] Hash containing item data
|
129
|
+
# @return [void]
|
130
|
+
def insert_item_from_hash(db, item)
|
131
|
+
fields = ['id', *ITEM_FIELDS].join(', ')
|
132
|
+
placeholders = Array.new(ITEM_FIELDS.length + 1, '?').join(', ')
|
133
|
+
db.execute_sql(
|
134
|
+
"INSERT INTO items (#{fields}) VALUES (#{placeholders})",
|
135
|
+
get_item_values(item, include_id_at_start: true)
|
136
|
+
)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Processes an item that exists in both databases
|
140
|
+
#
|
141
|
+
# @param id [Integer] Item ID
|
142
|
+
# @param local_item [Hash] Local database item
|
143
|
+
# @param remote_item [Hash] Remote database item
|
144
|
+
# @param local_db [SQLite3::Database] Local database connection
|
145
|
+
# @return [Symbol] :local_update if local was updated, :remote_update if remote needs update
|
146
|
+
def process_existing_item(*args)
|
147
|
+
id, local_item, remote_item, local_db = args
|
148
|
+
local_time = local_item['updated_at'].to_i
|
149
|
+
remote_time = remote_item['updated_at'].to_i
|
150
|
+
|
151
|
+
if remote_wins?(remote_item, remote_time, local_time)
|
152
|
+
puts format_status_message(id, remote_item, 'Remote')
|
153
|
+
update_item_from_hash(local_db, remote_item)
|
154
|
+
:local_update
|
155
|
+
elsif local_time > remote_time
|
156
|
+
puts format_status_message(id, local_item, 'Local')
|
157
|
+
:remote_update
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Converts database items to a hash indexed by ID
|
162
|
+
#
|
163
|
+
# @param items [Array<Hash>] Array of database items
|
164
|
+
# @return [Hash] Items indexed by ID
|
165
|
+
def items_to_hash(items)
|
166
|
+
items.to_h { |item| [item['id'], item] }
|
167
|
+
end
|
168
|
+
|
169
|
+
# Determines if remote item should take precedence
|
170
|
+
#
|
171
|
+
# @param remote_item [Hash] Remote database item
|
172
|
+
# @param remote_time [Integer] Remote item timestamp
|
173
|
+
# @param local_time [Integer] Local item timestamp
|
174
|
+
# @return [Boolean] true if remote item should take precedence
|
175
|
+
def remote_wins?(remote_item, remote_time, local_time)
|
176
|
+
remote_time > local_time && (remote_item['deleted'].to_i == 1 || remote_time > local_time)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Formats item status message
|
180
|
+
#
|
181
|
+
# @param id [Integer] Item ID
|
182
|
+
# @param item [Hash] Database item
|
183
|
+
# @param source [String] Source of the item ('Remote' or 'Local')
|
184
|
+
# @return [String] Formatted status message
|
185
|
+
def format_status_message(id, item, source)
|
186
|
+
deleted = item['deleted'].to_i == 1 ? ' and deleted' : ''
|
187
|
+
"#{source} item #{id} is newer#{deleted} - #{source == 'Remote' ? 'updating local' : 'will be uploaded'}"
|
188
|
+
end
|
189
|
+
|
190
|
+
# Updates an existing item in the database with values from a hash
|
191
|
+
#
|
192
|
+
# @param db [SQLite3::Database] The database connection
|
193
|
+
# @param item [Hash] Hash containing item data
|
194
|
+
# @return [void]
|
195
|
+
def update_item_from_hash(db, item)
|
196
|
+
fields = "#{ITEM_FIELDS.join(' = ?, ')} = ?"
|
197
|
+
db.execute_sql(
|
198
|
+
"UPDATE items SET #{fields} WHERE id = ?",
|
199
|
+
get_item_values(item)
|
200
|
+
)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Gets the values array for database operations
|
204
|
+
#
|
205
|
+
# @param item [Hash] Hash containing item data
|
206
|
+
# @param include_id [Boolean] Whether to include ID at start (insert) or end (update)
|
207
|
+
# @return [Array] Array of values for database operation
|
208
|
+
def get_item_values(item, include_id_at_start: false)
|
209
|
+
@database_fields ||= ITEM_FIELDS
|
210
|
+
values = @database_fields.map { |field| item[field] }
|
211
|
+
include_id_at_start ? [item['id'], *values] : values
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
data/lib/timet/s3_supabase.rb
CHANGED
@@ -179,6 +179,7 @@ module Timet
|
|
179
179
|
@logger.info "Object '#{object_key}' deleted successfully."
|
180
180
|
rescue Aws::S3::Errors::ServiceError => e
|
181
181
|
@logger.error "Error deleting object: #{e.message}"
|
182
|
+
raise e
|
182
183
|
end
|
183
184
|
|
184
185
|
# Deletes a bucket and all its contents.
|
@@ -197,6 +198,7 @@ module Timet
|
|
197
198
|
@logger.info "Bucket '#{bucket_name}' deleted successfully."
|
198
199
|
rescue Aws::S3::Errors::ServiceError => e
|
199
200
|
@logger.error "Error deleting bucket: #{e.message}"
|
201
|
+
raise e
|
200
202
|
end
|
201
203
|
|
202
204
|
private
|
@@ -207,14 +209,19 @@ module Timet
|
|
207
209
|
# @return [void]
|
208
210
|
def validate_env_vars
|
209
211
|
missing_vars = []
|
210
|
-
missing_vars
|
211
|
-
missing_vars
|
212
|
-
missing_vars
|
212
|
+
missing_vars.concat(check_env_var('S3_ENDPOINT', S3_ENDPOINT))
|
213
|
+
missing_vars.concat(check_env_var('S3_ACCESS_KEY', S3_ACCESS_KEY))
|
214
|
+
missing_vars.concat(check_env_var('S3_SECRET_KEY', S3_SECRET_KEY))
|
213
215
|
|
214
216
|
return if missing_vars.empty?
|
215
217
|
|
216
|
-
|
217
|
-
|
218
|
+
raise CustomError, "Missing required environment variables (.env): #{missing_vars.join(', ')}"
|
219
|
+
end
|
220
|
+
|
221
|
+
def check_env_var(name, value)
|
222
|
+
return [] if value && !value.empty?
|
223
|
+
|
224
|
+
[name]
|
218
225
|
end
|
219
226
|
|
220
227
|
# Custom error class that suppresses the backtrace for cleaner error messages.
|
data/lib/timet/table.rb
CHANGED
@@ -1,9 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# require_relative 'color_codes'
|
4
|
+
require 'timet/time_report_helper'
|
3
5
|
module Timet
|
4
|
-
# This
|
6
|
+
# This class is responsible for formatting the output of the `timet` application.
|
5
7
|
# It provides methods for formatting the table header, separators, and rows.
|
6
|
-
|
8
|
+
class Table
|
9
|
+
include TimeReportHelper
|
10
|
+
|
11
|
+
attr_reader :filter, :items
|
12
|
+
|
13
|
+
def initialize(filter, items, db)
|
14
|
+
@filter = filter
|
15
|
+
@items = items
|
16
|
+
@db = db
|
17
|
+
end
|
18
|
+
|
7
19
|
# Generates and displays a table summarizing time entries, including headers, time blocks, and total durations.
|
8
20
|
#
|
9
21
|
# @example
|
@@ -64,22 +76,26 @@ module Timet
|
|
64
76
|
|
65
77
|
# Processes time entries and generates a time block structure.
|
66
78
|
#
|
67
|
-
#
|
79
|
+
# This method iterates over each item in the `items` array, displays the time entry (if enabled),
|
80
|
+
# and processes the time block item to build a nested hash representing the time block structure.
|
81
|
+
#
|
82
|
+
# @param display [Boolean] Whether to display the time entry during processing. Defaults to `true`.
|
83
|
+
# @return [Hash] A nested hash representing the time block structure, where keys are dates and values
|
84
|
+
# are processed time block items.
|
68
85
|
#
|
69
86
|
# @note
|
70
|
-
# - The method
|
71
|
-
# -
|
72
|
-
# - It then processes the time block item using `process_time_block_item`.
|
87
|
+
# - The method uses `display_time_entry` to display the time entry if `display` is `true`.
|
88
|
+
# - It processes each time block item using `process_time_block_item`.
|
73
89
|
# - The `TimeHelper.extract_date` method is used to extract the date from the items.
|
74
90
|
#
|
75
91
|
# @see #display_time_entry
|
76
92
|
# @see #process_time_block_item
|
77
93
|
# @see TimeHelper#extract_date
|
78
|
-
def process_time_entries
|
94
|
+
def process_time_entries(display: true)
|
79
95
|
time_block = Hash.new { |hash, key| hash[key] = {} }
|
80
96
|
|
81
|
-
items.each_with_index do |item, idx|
|
82
|
-
display_time_entry(item, TimeHelper.extract_date(items, idx))
|
97
|
+
@items.each_with_index do |item, idx|
|
98
|
+
display_time_entry(item, TimeHelper.extract_date(@items, idx)) if display
|
83
99
|
time_block = process_time_block_item(item, time_block)
|
84
100
|
end
|
85
101
|
|
@@ -12,8 +12,8 @@ module Timet
|
|
12
12
|
# "2023-10-02" => { "10" => [4500, "work"] }
|
13
13
|
# }
|
14
14
|
# colors = { "work" => 31, "break" => 32 }
|
15
|
-
# chart = TimeBlockChart.new(
|
16
|
-
# chart.print_time_block_chart(
|
15
|
+
# chart = TimeBlockChart.new(table)
|
16
|
+
# chart.print_time_block_chart(table, colors)
|
17
17
|
#
|
18
18
|
# @attr_reader [Integer] start_hour The starting hour of the time block
|
19
19
|
# @attr_reader [Integer] end_hour The ending hour of the time block
|
@@ -34,22 +34,49 @@ module Timet
|
|
34
34
|
# Separator character for the chart
|
35
35
|
SEPARATOR_CHAR = '░'
|
36
36
|
|
37
|
-
# Initializes a new TimeBlockChart
|
37
|
+
# Initializes a new TimeBlockChart instance.
|
38
38
|
#
|
39
|
-
#
|
40
|
-
|
41
|
-
|
42
|
-
|
39
|
+
# This method sets up the time block chart by processing the time entries from the provided table
|
40
|
+
# and determining the start and end hours for the chart based on the time block data.
|
41
|
+
#
|
42
|
+
# @param table [Table] The table instance containing the time entries to be processed.
|
43
|
+
# @return [void] This method does not return a value; it initializes the instance variables.
|
44
|
+
#
|
45
|
+
# @note
|
46
|
+
# - The `@time_block` instance variable is populated by processing the time entries from the table.
|
47
|
+
# - The `@start_hour` and `@end_hour` instance variables are calculated based on the earliest and latest
|
48
|
+
# hours present in the time block data.
|
49
|
+
#
|
50
|
+
# @see Table#process_time_entries
|
51
|
+
def initialize(table)
|
52
|
+
@time_block = table.process_time_entries(display: false)
|
53
|
+
@start_hour = @time_block.values.map(&:keys).flatten.uniq.min.to_i
|
54
|
+
@end_hour = @time_block.values.map(&:keys).flatten.uniq.max.to_i
|
43
55
|
end
|
44
56
|
|
45
|
-
# Prints the time block chart
|
57
|
+
# Prints the time block chart.
|
46
58
|
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
|
59
|
+
# This method formats and prints the time block chart, including the header and the time blocks
|
60
|
+
# for each entry. The chart is color-coded based on the provided color mapping for different tags.
|
61
|
+
#
|
62
|
+
# @param table [Hash] The time block data to be displayed in the chart.
|
63
|
+
# @param colors [Hash] A mapping of tags to colors, used to color-code the time blocks.
|
64
|
+
# @return [void] This method does not return a value; it performs side effects such as printing the chart.
|
65
|
+
#
|
66
|
+
# @example Print a time block chart
|
67
|
+
# chart = TimeBlockChart.new(table)
|
68
|
+
# chart.print_time_block_chart(table, colors)
|
69
|
+
#
|
70
|
+
# @note
|
71
|
+
# - The method first prints the header of the chart, which includes the time range.
|
72
|
+
# - It then prints the time blocks, using the provided color mapping to visually distinguish
|
73
|
+
# between different tags.
|
74
|
+
#
|
75
|
+
# @see #print_header
|
76
|
+
# @see #print_blocks
|
77
|
+
def print_time_block_chart(table, colors)
|
51
78
|
print_header
|
52
|
-
print_blocks(
|
79
|
+
print_blocks(table, colors)
|
53
80
|
end
|
54
81
|
|
55
82
|
private
|
@@ -65,22 +92,44 @@ module Timet
|
|
65
92
|
puts '┌╴W ╴╴╴╴╴╴⏰╴╴╴╴╴╴┼'.gray + "#{'╴' * (@end_hour - @start_hour + 1) * 4}╴╴╴┼".gray
|
66
93
|
end
|
67
94
|
|
68
|
-
# Prints the time blocks
|
95
|
+
# Prints the time blocks for each date in the time block data.
|
69
96
|
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
|
74
|
-
|
97
|
+
# This method iterates over the time block data, formats and prints the date information,
|
98
|
+
# prints the time blocks for each date using the provided color mapping, and calculates
|
99
|
+
# and prints the total hours for each day. It also prints a footer at the end.
|
100
|
+
#
|
101
|
+
# @param table [Hash] The time block data containing the time entries for each date.
|
102
|
+
# @param colors [Hash] A mapping of tags to colors, used to color-code the time blocks.
|
103
|
+
# @return [void] This method does not return a value; it performs side effects such as printing
|
104
|
+
# the time blocks and related information.
|
105
|
+
#
|
106
|
+
# @example Print time blocks
|
107
|
+
# chart = TimeBlockChart.new(table)
|
108
|
+
# chart.print_blocks(table, colors)
|
109
|
+
#
|
110
|
+
# @note
|
111
|
+
# - The method skips processing if the `table` parameter is `nil`.
|
112
|
+
# - For each date in the time block data, it formats and prints the date and day of the week.
|
113
|
+
# - It prints the time blocks using the provided color mapping to visually distinguish
|
114
|
+
# between different tags.
|
115
|
+
# - It calculates and prints the total hours for each day.
|
116
|
+
# - A footer is printed at the end to provide a visual separation.
|
117
|
+
#
|
118
|
+
# @see #format_and_print_date_info
|
119
|
+
# @see #print_time_blocks
|
120
|
+
# @see #calculate_and_print_hours
|
121
|
+
# @see #print_footer
|
122
|
+
def print_blocks(table, colors)
|
123
|
+
return unless table
|
75
124
|
|
76
125
|
weeks = []
|
77
|
-
time_block.each_key do |date_string|
|
126
|
+
@time_block.each_key do |date_string|
|
78
127
|
date = Date.parse(date_string)
|
79
128
|
day = date.strftime('%a')[0..2]
|
80
129
|
|
81
130
|
format_and_print_date_info(date_string, day, weeks)
|
82
131
|
|
83
|
-
time_block_initial = time_block[date_string]
|
132
|
+
time_block_initial = @time_block[date_string]
|
84
133
|
print_time_blocks(time_block_initial, colors)
|
85
134
|
|
86
135
|
calculate_and_print_hours(time_block_initial)
|
data/lib/timet/time_report.rb
CHANGED
@@ -14,7 +14,6 @@ module Timet
|
|
14
14
|
# a formatted table with the relevant information.
|
15
15
|
class TimeReport
|
16
16
|
include TimeReportHelper
|
17
|
-
include Table
|
18
17
|
include TagDistribution
|
19
18
|
|
20
19
|
# Provides access to the database instance.
|
@@ -32,37 +31,55 @@ module Timet
|
|
32
31
|
# Initializes a new instance of the TimeReport class.
|
33
32
|
#
|
34
33
|
# @param db [Database] The database instance to use for fetching data.
|
35
|
-
# @param options [Hash] A hash containing optional parameters.
|
36
|
-
# @option options [String, nil] :filter The filter to apply when fetching items. Possible values include
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
# @
|
43
|
-
#
|
34
|
+
# @param options [Hash] A hash containing optional parameters for configuring the report.
|
35
|
+
# @option options [String, nil] :filter The filter to apply when fetching items. Possible values include:
|
36
|
+
# - 'today': Filters items for the current day.
|
37
|
+
# - 'yesterday': Filters items for the previous day.
|
38
|
+
# - 'week': Filters items for the current week.
|
39
|
+
# - 'month': Filters items for the current month.
|
40
|
+
# - A date range in the format 'YYYY-MM-DD..YYYY-MM-DD': Filters items within the specified date range.
|
41
|
+
# @option options [String, nil] :tag The tag to filter the items by. Only items with this tag will be included.
|
42
|
+
# @option options [String, nil] :csv The filename to use when exporting the report to CSV. If provided, the report
|
43
|
+
# will be exported to the specified file.
|
44
|
+
# @option options [String, nil] :ics The filename to use when exporting the report to iCalendar format. If provided,
|
45
|
+
# the report will be exported to the specified file.
|
46
|
+
#
|
47
|
+
# @return [void] This method does not return a value; it initializes the instance variables and prepares the report.
|
44
48
|
#
|
45
49
|
# @example Initialize a new TimeReport instance with a filter and tag
|
46
50
|
# TimeReport.new(db, filter: 'today', tag: 'work', csv: 'report.csv', ics: 'icalendar.ics')
|
51
|
+
#
|
52
|
+
# @example Initialize a new TimeReport instance with a date range filter
|
53
|
+
# TimeReport.new(db, filter: '2023-10-01..2023-10-31', tag: 'project')
|
54
|
+
#
|
55
|
+
# @note
|
56
|
+
# - If no filter is provided, all items from the database will be fetched.
|
57
|
+
# - The `@table` instance variable is initialized with the filtered items and filter configuration.
|
47
58
|
def initialize(db, options = {})
|
48
59
|
@db = db
|
49
60
|
@csv_filename = options[:csv]
|
50
61
|
@ics_filename = options[:ics]
|
51
62
|
@filter = formatted_filter(options[:filter])
|
52
63
|
@items = options[:filter] ? filter_items(@filter, options[:tag]) : @db.all_items
|
64
|
+
@table = Table.new(@filter, @items, @db)
|
53
65
|
end
|
54
66
|
|
55
67
|
# Displays the report of tracked time entries.
|
56
68
|
#
|
69
|
+
# This method formats and prints the report, including the table header, rows, total duration,
|
70
|
+
# a time block chart, and tag distribution. If no tracked time entries are found for the specified filter,
|
71
|
+
# it displays a message indicating no data is available.
|
72
|
+
#
|
57
73
|
# @return [void] This method does not return a value; it performs side effects such as printing the report.
|
58
74
|
#
|
59
75
|
# @example Display the report
|
60
76
|
# time_report.display
|
61
77
|
#
|
62
|
-
# @note
|
63
|
-
#
|
64
|
-
#
|
65
|
-
#
|
78
|
+
# @note
|
79
|
+
# - The method checks if there are any tracked time entries. If not, it prints a message and exits.
|
80
|
+
# - It uses the `@table` instance to format and display the table.
|
81
|
+
# - A time block chart is generated and printed using the `TimeBlockChart` class.
|
82
|
+
# - The tag distribution is calculated and displayed based on the unique colors assigned to tags.
|
66
83
|
#
|
67
84
|
# @see #table
|
68
85
|
# @see #print_time_block_chart
|
@@ -70,30 +87,38 @@ module Timet
|
|
70
87
|
def display
|
71
88
|
return puts 'No tracked time found for the specified filter.' if @items.empty?
|
72
89
|
|
73
|
-
|
74
|
-
|
90
|
+
@table.table
|
75
91
|
colors = @items.map { |x| x[3] }.uniq.each_with_index.to_h
|
76
|
-
chart = TimeBlockChart.new(
|
77
|
-
chart.print_time_block_chart(
|
78
|
-
|
92
|
+
chart = TimeBlockChart.new(@table)
|
93
|
+
chart.print_time_block_chart(@table, colors)
|
79
94
|
tag_distribution(colors)
|
80
95
|
end
|
81
96
|
|
82
97
|
# Displays a single row of the report.
|
83
98
|
#
|
84
|
-
#
|
99
|
+
# This method formats and prints a single row of the report, including the table header, the specified row,
|
100
|
+
# a separator, and the total duration. It is used to display individual time entries in a structured format.
|
101
|
+
#
|
102
|
+
# @param item [Array] The item (time entry) to display. The item is expected to contain the necessary data
|
103
|
+
# for the row, such as the time, description, and duration.
|
85
104
|
#
|
86
105
|
# @return [void] This method does not return a value; it performs side effects such as printing the row.
|
87
106
|
#
|
88
107
|
# @example Display a single row
|
89
108
|
# time_report.show_row(item)
|
90
109
|
#
|
91
|
-
# @note
|
110
|
+
# @note
|
111
|
+
# - The method uses the `@table` instance to format and display the table header and row.
|
112
|
+
# - A separator is printed after the row to visually distinguish it from other rows.
|
113
|
+
# - The total duration is displayed at the end of the row.
|
114
|
+
#
|
115
|
+
# @see #table
|
116
|
+
# @see #display_time_entry
|
92
117
|
def show_row(item)
|
93
|
-
header
|
94
|
-
display_time_entry(item)
|
95
|
-
puts separator
|
96
|
-
total
|
118
|
+
@table.header
|
119
|
+
@table.display_time_entry(item)
|
120
|
+
puts @table.separator
|
121
|
+
@table.total
|
97
122
|
end
|
98
123
|
|
99
124
|
private
|
@@ -83,18 +83,18 @@ module Timet
|
|
83
83
|
#
|
84
84
|
# @param item [Array] The tracking item to be updated.
|
85
85
|
# @param field [String] The time field to be updated.
|
86
|
-
# @param
|
86
|
+
# @param new_time [String] The new time value.
|
87
87
|
#
|
88
88
|
# @return [Time] The updated time value.
|
89
89
|
#
|
90
90
|
# @example Update the 'start' field of a tracking item with a formatted date value
|
91
|
-
# update_time_field(item, 'start', '
|
92
|
-
def update_time_field(item, field,
|
91
|
+
# update_time_field(item, 'start', '11:10:00')
|
92
|
+
def update_time_field(item, field, new_time)
|
93
93
|
field_index = Timet::Application::FIELD_INDEX[field]
|
94
94
|
timestamp = item[field_index]
|
95
|
-
|
96
|
-
|
97
|
-
DateTime.strptime(
|
95
|
+
edit_time = Time.at(timestamp || item[1]).to_s.split
|
96
|
+
edit_time[1] = new_time
|
97
|
+
DateTime.strptime(edit_time.join(' '), '%Y-%m-%d %H:%M:%S %z').to_time
|
98
98
|
end
|
99
99
|
|
100
100
|
# Validates if a new time value is valid for a specific time field (start or end).
|
data/lib/timet/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: timet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.5.
|
4
|
+
version: 1.5.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Frank Vielma
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-02-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -106,14 +106,14 @@ dependencies:
|
|
106
106
|
requirements:
|
107
107
|
- - "~>"
|
108
108
|
- !ruby/object:Gem::Version
|
109
|
-
version: '1.
|
109
|
+
version: '1.178'
|
110
110
|
type: :runtime
|
111
111
|
prerelease: false
|
112
112
|
version_requirements: !ruby/object:Gem::Requirement
|
113
113
|
requirements:
|
114
114
|
- - "~>"
|
115
115
|
- !ruby/object:Gem::Version
|
116
|
-
version: '1.
|
116
|
+
version: '1.178'
|
117
117
|
description: Timet is a command-line time tracker that keeps track of your activities.
|
118
118
|
email:
|
119
119
|
- frankvielma@gmail.com
|
@@ -139,6 +139,7 @@ files:
|
|
139
139
|
- lib/timet/color_codes.rb
|
140
140
|
- lib/timet/database.rb
|
141
141
|
- lib/timet/database_sync_helper.rb
|
142
|
+
- lib/timet/database_syncer.rb
|
142
143
|
- lib/timet/s3_supabase.rb
|
143
144
|
- lib/timet/table.rb
|
144
145
|
- lib/timet/tag_distribution.rb
|