fewald-worklog 0.2.19 → 0.2.21

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: e2bd1127e65b1c43b9ae95773b29d5648818940b44cfa82be0e69741148f3b16
4
- data.tar.gz: e30b8828d79177651e0179a2a7ec915fd03075de857e47280eaaec8bf682b8b4
3
+ metadata.gz: a799c589dedc9f7ed68ef9b2acfd8ad08e29f3fb5f3a26d0a90dee1320ee164e
4
+ data.tar.gz: 9e38d05b4303b41cf7ae2016d23990290ef2b41eba34db9b8c53f4a7587a84d2
5
5
  SHA512:
6
- metadata.gz: 05f0bc3f9acd0ebe71b8fd78608c4227c0a54c6b54c711fb657eaa6d1ce24e952bf8e37785327745cfb100a8574705d1441b12110fed80b6103f24b2532761aa
7
- data.tar.gz: 31130d1928c6420c7c681d796189ccfc696875e92befb981463034d3d12c299f6aaab0ef3634c87fa787311e0042e3ff2cb8232be0db6a759505753446a998aa
6
+ metadata.gz: a9cc6c2ef435e43390491c2d31793c4e2ae8febd54bdd68ea535decb109defd70538bdaaaa1dc70d660c0f1193e9786d4bab66cf1a0619bf5166146b1c8eb95c
7
+ data.tar.gz: aa43dfa148848d4161a791105934e36c9caf21929d4e978e3b20468fe076b288b555137cf7b74f06fb17157f9312fcc40050894e494c176334679d61033fa198
data/.version CHANGED
@@ -1 +1 @@
1
- 0.2.19
1
+ 0.2.21
data/lib/cli.rb CHANGED
@@ -31,7 +31,7 @@ class WorklogCLI < Thor
31
31
  # Initialize the CLI with the given arguments, options, and configuration
32
32
  def initialize(args = [], options = {}, config = {})
33
33
  @config = Worklog::Configuration.load
34
- @storage = Storage.new(@config)
34
+ @storage = Worklog::Storage.new(@config)
35
35
  super
36
36
  end
37
37
 
data/lib/daily_log.rb CHANGED
@@ -2,52 +2,54 @@
2
2
 
3
3
  require 'hash'
4
4
 
5
- # DailyLog is a container for a day's work log.
6
- class DailyLog
7
- # Container for a day's work log.
8
- include Hashify
9
-
10
- # Represents a single day's work log.
11
- attr_accessor :date, :entries
12
-
13
- def initialize(params = {})
14
- @date = params[:date]
15
- @entries = params[:entries]
16
- end
17
-
18
- # Returns true if there are people mentioned in any entry of the current day.
19
- #
20
- # @return [Boolean] true if there are people mentioned, false otherwise.
21
- def people?
22
- people.size.positive?
23
- end
24
-
25
- # Returns a hash of people mentioned in the log for the current day
26
- # with the number of times they are mentioned.
27
- # People are defined as words starting with @ or ~.
28
- #
29
- # @return [Hash<String, Integer>]
30
- def people
31
- entries.map { |entry| entry.people.to_a }.flatten.tally
32
- end
33
-
34
- # Returns a sorted list of tags used in the entries for the current day.
35
- #
36
- # @return [Array<String>]
37
- #
38
- # @example
39
- # log = DailyLog.new(date: Date.today,
40
- # entries: [LogEntry.new(message: "Work on something", tags: ['work', 'project'])])
41
- # log.tags # => ["project", "work"]
42
- def tags
43
- entries.flat_map(&:tags).uniq.sort
44
- end
45
-
46
- # Equals method to compare two DailyLog objects.
47
- #
48
- # @param other [DailyLog] the other DailyLog object to compare with
49
- # @return [Boolean] true if both DailyLog objects have the same date and entries, false otherwise
50
- def ==(other)
51
- date == other.date && entries == other.entries
5
+ module Worklog
6
+ # DailyLog is a container for a day's work log.
7
+ class DailyLog
8
+ # Container for a day's work log.
9
+ include Hashify
10
+
11
+ # Represents a single day's work log.
12
+ attr_accessor :date, :entries
13
+
14
+ def initialize(params = {})
15
+ @date = params[:date]
16
+ @entries = params[:entries]
17
+ end
18
+
19
+ # Returns true if there are people mentioned in any entry of the current day.
20
+ #
21
+ # @return [Boolean] true if there are people mentioned, false otherwise.
22
+ def people?
23
+ people.size.positive?
24
+ end
25
+
26
+ # Returns a hash of people mentioned in the log for the current day
27
+ # with the number of times they are mentioned.
28
+ # People are defined as words starting with @ or ~.
29
+ #
30
+ # @return [Hash<String, Integer>]
31
+ def people
32
+ entries.map { |entry| entry.people.to_a }.flatten.tally
33
+ end
34
+
35
+ # Returns a sorted list of tags used in the entries for the current day.
36
+ #
37
+ # @return [Array<String>]
38
+ #
39
+ # @example
40
+ # log = DailyLog.new(date: Date.today,
41
+ # entries: [LogEntry.new(message: "Work on something", tags: ['work', 'project'])])
42
+ # log.tags # => ["project", "work"]
43
+ def tags
44
+ entries.flat_map(&:tags).uniq.sort
45
+ end
46
+
47
+ # Equals method to compare two DailyLog objects.
48
+ #
49
+ # @param other [DailyLog] the other DailyLog object to compare with
50
+ # @return [Boolean] true if both DailyLog objects have the same date and entries, false otherwise
51
+ def ==(other)
52
+ date == other.date && entries == other.entries
53
+ end
52
54
  end
53
55
  end
data/lib/log_entry.rb CHANGED
@@ -5,95 +5,117 @@ require 'rainbow'
5
5
  require 'daily_log'
6
6
  require 'hash'
7
7
 
8
- # A single log entry.
9
- class LogEntry
10
- PERSON_REGEX = /(?:\s|^)[~@](\w+)/
11
-
12
- include Hashify
13
-
14
- # Represents a single entry in the work log.
15
- attr_accessor :time, :tags, :ticket, :url, :epic, :message, :project
16
-
17
- attr_reader :day
18
-
19
- def initialize(params = {})
20
- @time = params[:time]
21
- # If tags are nil, set to empty array.
22
- # This is similar to the CLI default value.
23
- @tags = params[:tags] || []
24
- @ticket = params[:ticket]
25
- @url = params[:url] || ''
26
- @epic = params[:epic]
27
- @message = params[:message]
28
- @project = params[:project]
29
-
30
- # Back reference to the day
31
- @day = params[:day] || nil
32
- end
8
+ module Worklog
9
+ # A single log entry in a DailyLog.
10
+ # @see DailyLog
11
+ # @!attribute [rw] time
12
+ # @return [DateTime] the date and time of the log entry.
13
+ # @!attribute [rw] tags
14
+ # @return [Array<String>] the tags associated with the log entry.
15
+ # @!attribute [rw] ticket
16
+ # @return [String] the ticket associated with the log entry.
17
+ # @!attribute [rw] url
18
+ # @return [String] the URL associated with the log entry.
19
+ # @!attribute [rw] epic
20
+ # @return [Boolean] whether the log entry is an epic.
21
+ # @!attribute [rw] message
22
+ # @return [String] the message of the log entry.
23
+ # @!attribute [rw] project
24
+ # @return [String] the project associated with the log entry.
25
+ class LogEntry
26
+ PERSON_REGEX = /(?:\s|^)[~@](\w+)/
27
+
28
+ include Hashify
29
+
30
+ attr_accessor :time, :tags, :ticket, :url, :epic, :message, :project
31
+
32
+ attr_reader :day
33
+
34
+ def initialize(params = {})
35
+ @time = params[:time]
36
+ # If tags are nil, set to empty array.
37
+ # This is similar to the CLI default value.
38
+ @tags = params[:tags] || []
39
+ @ticket = params[:ticket]
40
+ @url = params[:url] || ''
41
+ @epic = params[:epic]
42
+ @message = params[:message]
43
+ @project = params[:project]
44
+
45
+ # Back reference to the day
46
+ @day = params[:day] || nil
47
+ end
33
48
 
34
- # Returns true if the entry is an epic, false otherwise.
35
- def epic?
36
- @epic == true
37
- end
49
+ # Returns true if the entry is an epic, false otherwise.
50
+ # @return [Boolean]
51
+ def epic?
52
+ @epic == true
53
+ end
38
54
 
39
- # Returns the message string with formatting without the time.
40
- # @param known_people Hash[String, Person] A hash of people with their handles as keys.
41
- def message_string(known_people = nil)
42
- # replace all mentions of people with their names.
43
- msg = @message.dup
44
- people.each do |person|
45
- next unless known_people && known_people[person]
46
-
47
- msg.gsub!(/[~@]#{person}/) do |match|
48
- s = ''
49
- s += ' ' if match[0] == ' '
50
- s += "#{Rainbow(known_people[person].name).underline} (~#{person})" if known_people && known_people[person]
51
- s
55
+ # Returns the message string with formatting without the time.
56
+ # @param known_people Hash[String, Person] A hash of people with their handles as keys.
57
+ def message_string(known_people = nil)
58
+ # replace all mentions of people with their names.
59
+ msg = @message.dup
60
+ people.each do |person|
61
+ next unless known_people && known_people[person]
62
+
63
+ msg.gsub!(/[~@]#{person}/) do |match|
64
+ s = ''
65
+ s += ' ' if match[0] == ' '
66
+ s += "#{Rainbow(known_people[person].name).underline} (~#{person})" if known_people && known_people[person]
67
+ s
68
+ end
52
69
  end
53
- end
54
70
 
55
- s = ''
71
+ s = ''
56
72
 
57
- s += if epic
58
- Rainbow("[EPIC] #{msg}").bg(:white).fg(:black)
59
- else
60
- msg
61
- end
73
+ s += if epic
74
+ Rainbow("[EPIC] #{msg}").bg(:white).fg(:black)
75
+ else
76
+ msg
77
+ end
62
78
 
63
- s += " [#{Rainbow(@ticket).fg(:blue)}]" if @ticket
79
+ s += " [#{Rainbow(@ticket).fg(:blue)}]" if @ticket
64
80
 
65
- # Add tags in brackets if defined.
66
- s += ' [' + @tags.map { |tag| "#{tag}" }.join(', ') + ']' if @tags && @tags.size > 0
81
+ # Add tags in brackets if defined.
82
+ s += ' [' + @tags.map { |tag| "#{tag}" }.join(', ') + ']' if @tags && @tags.size > 0
67
83
 
68
- # Add URL in brackets if defined.
69
- s += " [#{@url}]" if @url && @url != ''
84
+ # Add URL in brackets if defined.
85
+ s += " [#{@url}]" if @url && @url != ''
70
86
 
71
- s += " [#{@project}]" if @project && @project != ''
87
+ s += " [#{@project}]" if @project && @project != ''
72
88
 
73
- s
74
- end
89
+ s
90
+ end
75
91
 
76
- def people
77
- # Return people that are mentioned in the entry. People are defined as character sequences
78
- # starting with @ or ~. Whitespaces are used to separate people. Punctuation is ignored.
79
- # Empty set if no people are mentioned.
80
- # @return [Set<String>]
81
- @message.scan(PERSON_REGEX).flatten.uniq.sort.to_set
82
- end
92
+ def people
93
+ # Return people that are mentioned in the entry. People are defined as character sequences
94
+ # starting with @ or ~. Whitespaces are used to separate people. Punctuation is ignored.
95
+ # Empty set if no people are mentioned.
96
+ # @return [Set<String>]
97
+ @message.scan(PERSON_REGEX).flatten.uniq.sort.to_set
98
+ end
83
99
 
84
- def people?
85
100
  # Return true if there are people in the entry.
86
101
  #
87
102
  # @return [Boolean]
88
- people.size.positive?
89
- end
103
+ def people?
104
+ people.size.positive?
105
+ end
90
106
 
91
- def to_yaml
92
- to_hash.to_yaml
93
- end
107
+ # Convert the log entry to YAML format.
108
+ def to_yaml
109
+ to_hash.to_yaml
110
+ end
94
111
 
95
- def ==(other)
96
- time == other.time && tags == other.tags && ticket == other.ticket && url == other.url &&
97
- epic == other.epic && message == other.message
112
+ # Compare two log entries for equality.
113
+ #
114
+ # @param other [LogEntry] The other log entry to compare against.
115
+ # @return [Boolean] True if the log entries are equal, false otherwise.
116
+ def ==(other)
117
+ time == other.time && tags == other.tags && ticket == other.ticket && url == other.url &&
118
+ epic == other.epic && message == other.message
119
+ end
98
120
  end
99
121
  end
data/lib/printer.rb CHANGED
@@ -9,7 +9,7 @@ class Printer
9
9
  # Initializes the printer with a list of people.
10
10
  # @param people [Array<Person>] An array of Person objects.
11
11
  def initialize(people = nil)
12
- @people = (people || []).to_h { |person| [person.handle, person] }
12
+ @people = people || {}
13
13
  end
14
14
 
15
15
  # Prints a whole day of work log entries.
data/lib/project.rb CHANGED
@@ -17,10 +17,18 @@ module Worklog
17
17
  # @return [String, nil] The status of the project, can be nil
18
18
  # Possible values: 'active', 'completed', 'archived', etc.
19
19
  # Indicates the current state of the project.
20
+ # @!attribute [rw] entries
21
+ # These entries are related to the work done on this project.
22
+ # Entries are populated dynamically when processing daily logs.
23
+ # They are not stored in the project itself.
24
+ # @return [Array<LogEntry>] An array of log entries associated with the project.
20
25
  # @!attribute [rw] last_activity
21
- # @return [Date, nil] The last activity date or nil if not set.
26
+ # The last activity is not stored in the project itself.
27
+ # Instead, it is updated dynamically when processing daily logs.
28
+ # It represents the most recent log entry time for this project.
29
+ # @return [Date, nil] The last activity date or nil if not set.
22
30
  class Project
23
- attr_accessor :key, :name, :description, :start_date, :end_date, :status, :last_activity
31
+ attr_accessor :key, :name, :description, :start_date, :end_date, :status, :entries, :last_activity
24
32
 
25
33
  # Creates a new Project instance from a hash of attributes.
26
34
  # @param hash [Hash] A hash containing project attributes
data/lib/statistics.rb CHANGED
@@ -10,7 +10,7 @@ class Statistics
10
10
  # Initialize the Statistics class.
11
11
  def initialize(config)
12
12
  @config = config
13
- @storage = Storage.new(config)
13
+ @storage = Worklog::Storage.new(config)
14
14
  end
15
15
 
16
16
  # Calculate statistics for the work log for all days.
data/lib/storage.rb CHANGED
@@ -6,176 +6,186 @@ require 'log_entry'
6
6
  require 'worklogger'
7
7
  require 'person'
8
8
 
9
- # Handles storage of daily logs and people
10
- class Storage
11
- # LogNotFoundError is raised when a log file is not found
12
- class LogNotFoundError < StandardError; end
9
+ # Alias for classes to handle existing log entries
10
+ DailyLog = Worklog::DailyLog
11
+ LogEntry = Worklog::LogEntry
13
12
 
14
- FILE_SUFFIX = '.yaml'
13
+ module Worklog
14
+ # Handles storage of daily logs and people
15
+ class Storage
16
+ # LogNotFoundError is raised when a log file is not found
17
+ class LogNotFoundError < StandardError; end
15
18
 
16
- # Regular expression to match daily log file names
17
- LOG_PATTERN = /\d{4}-\d{2}-\d{2}#{FILE_SUFFIX}\z/
19
+ FILE_SUFFIX = '.yaml'
18
20
 
19
- def initialize(config)
20
- @config = config
21
- end
21
+ # Regular expression to match daily log file names
22
+ LOG_PATTERN = /\d{4}-\d{2}-\d{2}#{FILE_SUFFIX}\z/
22
23
 
23
- def folder_exists?
24
- Dir.exist?(@config.storage_path)
25
- end
24
+ def initialize(config)
25
+ @config = config
26
+ end
26
27
 
27
- # Return all logs for all available days
28
- # @return [Array<DailyLog>] List of all logs
29
- def all_days
30
- return [] unless folder_exists?
28
+ def folder_exists?
29
+ Dir.exist?(@config.storage_path)
30
+ end
31
31
 
32
- logs = []
33
- Dir.glob(File.join(@config.storage_path, "*#{FILE_SUFFIX}")).map do |file|
34
- next unless file.match?(LOG_PATTERN)
32
+ # Return all logs for all available days
33
+ # @return [Array<DailyLog>] List of all logs
34
+ def all_days
35
+ return [] unless folder_exists?
35
36
 
36
- logs << load_log(file)
37
- end
37
+ logs = []
38
+ Dir.glob(File.join(@config.storage_path, "*#{FILE_SUFFIX}")).map do |file|
39
+ next unless file.match?(LOG_PATTERN)
38
40
 
39
- logs
40
- end
41
+ logs << load_log(file)
42
+ end
43
+
44
+ logs
45
+ end
41
46
 
42
- # Return all tags as a set
43
- # @return [Set<String>] Set of all tags
44
- def tags
45
- logs = all_days
46
- tags = Set[]
47
- logs.each do |log|
48
- log.entries.each do |entry|
49
- next unless entry.tags
50
-
51
- entry.tags.each do |tag|
52
- tags << tag
47
+ # Return all tags as a set
48
+ # @return [Set<String>] Set of all tags
49
+ def tags
50
+ logs = all_days
51
+ tags = Set[]
52
+ logs.each do |log|
53
+ log.entries.each do |entry|
54
+ next unless entry.tags
55
+
56
+ entry.tags.each do |tag|
57
+ tags << tag
58
+ end
53
59
  end
54
60
  end
61
+ tags
55
62
  end
56
- tags
57
- end
58
63
 
59
- # Return days between start_date and end_date
60
- # If end_date is nil, return logs from start_date to today
61
- #
62
- # @param [Date] start_date The start date, inclusive
63
- # @param [Date] end_date The end date, inclusive
64
- # @param [Boolean] epics_only If true, only return logs with epic entries
65
- # @param [Array<String>] tags_filter If provided, only return logs with entries that have at least one of the tags
66
- # @return [Array<DailyLog>] List of logs
67
- def days_between(start_date, end_date = nil, epics_only = nil, tags_filter = nil)
68
- return [] unless folder_exists?
69
-
70
- logs = []
71
- end_date = Date.today if end_date.nil?
72
-
73
- return [] if start_date > end_date
74
-
75
- while start_date <= end_date
76
- if File.exist?(filepath(start_date))
77
- tmp_logs = load_log!(filepath(start_date))
78
- tmp_logs.entries.keep_if { |entry| entry.epic? } if epics_only
79
-
80
- if tags_filter
81
- # Safeguard against entries without any tags, not just empty array
82
- tmp_logs.entries.keep_if { |entry| entry.tags && (entry.tags & tags_filter).size > 0 }
64
+ # Return days between start_date and end_date
65
+ # If end_date is nil, return logs from start_date to today
66
+ #
67
+ # @param [Date] start_date The start date, inclusive
68
+ # @param [Date] end_date The end date, inclusive
69
+ # @param [Boolean] epics_only If true, only return logs with epic entries
70
+ # @param [Array<String>] tags_filter If provided, only return logs with entries that have at least one of the tags
71
+ # @return [Array<DailyLog>] List of logs
72
+ def days_between(start_date, end_date = nil, epics_only = nil, tags_filter = nil)
73
+ return [] unless folder_exists?
74
+
75
+ logs = []
76
+ end_date = Date.today if end_date.nil?
77
+
78
+ return [] if start_date > end_date
79
+
80
+ while start_date <= end_date
81
+ if File.exist?(filepath(start_date))
82
+ tmp_logs = load_log!(filepath(start_date))
83
+ tmp_logs.entries.keep_if { |entry| entry.epic? } if epics_only
84
+
85
+ if tags_filter
86
+ # Safeguard against entries without any tags, not just empty array
87
+ tmp_logs.entries.keep_if { |entry| entry.tags && (entry.tags & tags_filter).size > 0 }
88
+ end
89
+
90
+ logs << tmp_logs if tmp_logs.entries.length > 0
83
91
  end
84
92
 
85
- logs << tmp_logs if tmp_logs.entries.length > 0
93
+ start_date += 1
86
94
  end
95
+ logs
96
+ end
87
97
 
88
- start_date += 1
98
+ # Create file for a new day if it does not exist
99
+ # @param [Date] date The date, used as the file name.
100
+ def create_file_skeleton(date)
101
+ File.write(filepath(date), YAML.dump(DailyLog.new(date:, entries: []))) unless File.exist?(filepath(date))
89
102
  end
90
- logs
91
- end
92
103
 
93
- # Create file for a new day if it does not exist
94
- # @param [Date] date The date, used as the file name.
95
- def create_file_skeleton(date)
96
- File.write(filepath(date), YAML.dump(DailyLog.new(date:, entries: []))) unless File.exist?(filepath(date))
97
- end
104
+ def load_log(file)
105
+ load_log!(file)
106
+ rescue LogNotFoundError
107
+ WorkLogger.error "No work log found for #{file}. Aborting."
108
+ nil
109
+ end
98
110
 
99
- def load_log(file)
100
- load_log!(file)
101
- rescue LogNotFoundError
102
- WorkLogger.error "No work log found for #{file}. Aborting."
103
- nil
104
- end
111
+ def load_log!(file)
112
+ WorkLogger.debug "Loading file #{file}"
113
+
114
+ # Alias DailyLog to Worklog::DailyLog
105
115
 
106
- def load_log!(file)
107
- WorkLogger.debug "Loading file #{file}"
108
- begin
109
- log = YAML.load_file(file, permitted_classes: [Date, Time, DailyLog, LogEntry])
110
- log.entries.each do |entry|
111
- entry.time = Time.parse(entry.time) unless entry.time.respond_to?(:strftime)
116
+ begin
117
+ log = YAML.load_file(file, permitted_classes: [Date, Time, DailyLog, LogEntry])
118
+ log.entries.each do |entry|
119
+ entry.time = Time.parse(entry.time) unless entry.time.respond_to?(:strftime)
120
+ end
121
+ log
122
+ rescue Errno::ENOENT
123
+ raise LogNotFoundError
112
124
  end
113
- log
114
- rescue Errno::ENOENT
115
- raise LogNotFoundError
116
125
  end
117
- end
118
126
 
119
- def write_log(file, daily_log)
120
- WorkLogger.debug "Writing to file #{file}"
127
+ def write_log(file, daily_log)
128
+ WorkLogger.debug "Writing to file #{file}"
121
129
 
122
- File.open(file, 'w') do |f|
123
- f.puts daily_log.to_yaml
130
+ File.open(file, 'w') do |f|
131
+ f.puts daily_log.to_yaml
132
+ end
124
133
  end
125
- end
126
134
 
127
- def load_single_log_file(file, headline = true)
128
- daily_log = load_log!(file)
129
- puts "Work log for #{Rainbow(daily_log.date).gold}:" if headline
130
- daily_log.entries
131
- end
135
+ # Load a single log file and return its entries
136
+ def load_single_log_file(file, headline = true)
137
+ daily_log = load_log!(file)
138
+ puts "Work log for #{Rainbow(daily_log.date).gold}:" if headline
139
+ daily_log.entries
140
+ end
132
141
 
133
- # Load all people from the people file, or return an empty array if the file does not exist
134
- #
135
- # @return [Array<Person>] List of people
136
- def load_people
137
- load_people!
138
- rescue Errno::ENOENT
139
- WorkLogger.info 'Unable to load people.'
140
- []
141
- end
142
+ # Load all people from the people file, or return an empty array if the file does not exist
143
+ #
144
+ # @return [Array<Person>] List of people
145
+ def load_people
146
+ load_people!
147
+ rescue Errno::ENOENT
148
+ WorkLogger.info 'Unable to load people.'
149
+ []
150
+ end
142
151
 
143
- # Load all people from the people file and return them as a hash with handle as key
144
- # @return [Hash<String, Person>] Hash of people with handle as key
145
- def load_people_hash
146
- load_people.to_h { |person| [person.handle, person] }
147
- end
152
+ # Load all people from the people file and return them as a hash with handle as key
153
+ # @return [Hash<String, Person>] Hash of people with handle as key
154
+ def load_people_hash
155
+ load_people.to_h { |person| [person.handle, person] }
156
+ end
148
157
 
149
- # Load all people from the people file
150
- # @return [Array<Person>] List of people
151
- def load_people!
152
- people_file = File.join(@config.storage_path, 'people.yaml')
153
- return [] unless File.exist?(people_file)
158
+ # Load all people from the people file
159
+ # @return [Array<Person>] List of people
160
+ def load_people!
161
+ people_file = File.join(@config.storage_path, 'people.yaml')
162
+ return [] unless File.exist?(people_file)
154
163
 
155
- YAML.load_file(people_file, permitted_classes: [Person])
156
- end
164
+ YAML.load_file(people_file, permitted_classes: [Person])
165
+ end
157
166
 
158
- # Write people to the people file
159
- # @param [Array<Person>] people List of people
160
- def write_people!(people)
161
- people_file = File.join(@config.storage_path, 'people.yaml')
162
- File.open(people_file, 'w') do |f|
163
- f.puts people.to_yaml
167
+ # Write people to the people file
168
+ # @param [Array<Person>] people List of people
169
+ def write_people!(people)
170
+ people_file = File.join(@config.storage_path, 'people.yaml')
171
+ File.open(people_file, 'w') do |f|
172
+ f.puts people.to_yaml
173
+ end
164
174
  end
165
- end
166
175
 
167
- # Create folder if not exists already.
168
- def create_default_folder
169
- # Do nothing if the storage path is not the default path
170
- return unless @config.default_storage_path?
176
+ # Create folder if not exists already.
177
+ def create_default_folder
178
+ # Do nothing if the storage path is not the default path
179
+ return unless @config.default_storage_path?
171
180
 
172
- Dir.mkdir(@config.storage_path) unless Dir.exist?(@config.storage_path)
173
- end
181
+ Dir.mkdir(@config.storage_path) unless Dir.exist?(@config.storage_path)
182
+ end
174
183
 
175
- # Construct filepath for a given date.
176
- # @param [Date] date The date
177
- # @return [String] The filepath
178
- def filepath(date)
179
- File.join(@config.storage_path, "#{date}#{FILE_SUFFIX}")
184
+ # Construct filepath for a given date.
185
+ # @param [Date] date The date
186
+ # @return [String] The filepath
187
+ def filepath(date)
188
+ File.join(@config.storage_path, "#{date}#{FILE_SUFFIX}")
189
+ end
180
190
  end
181
191
  end
data/lib/worklog.rb CHANGED
@@ -54,6 +54,9 @@ module Worklog
54
54
  # Bootstrap the worklog application.
55
55
  def bootstrap
56
56
  @storage.create_default_folder
57
+
58
+ # Load all people as they're used in multiple/most of the methods.
59
+ @people = @storage.load_people_hash
57
60
  end
58
61
 
59
62
  # Add new entry to the work log.
@@ -89,8 +92,7 @@ module Worklog
89
92
 
90
93
  @storage.write_log(@storage.filepath(options[:date]), daily_log)
91
94
 
92
- people_hash = @storage.load_people_hash
93
- (new_entry.people - people_hash.keys).each do |handle|
95
+ (new_entry.people - @people.keys).each do |handle|
94
96
  WorkLogger.warn "Person with handle #{handle} not found. Consider adding them to people.yaml"
95
97
  end
96
98
 
@@ -130,8 +132,7 @@ module Worklog
130
132
  # worklog.show(from: '2023-10-01', to: '2023-10-31')
131
133
  # worklog.show(date: '2023-10-01')
132
134
  def show(options = {})
133
- people = @storage.load_people!
134
- printer = Printer.new(people)
135
+ printer = Printer.new(@people)
135
136
 
136
137
  start_date, end_date = start_end_date(options)
137
138
 
@@ -145,17 +146,16 @@ module Worklog
145
146
  end
146
147
  end
147
148
 
149
+ # Show all known people and details about a specific person.
148
150
  def people(person = nil, _options = {})
149
- all_people = @storage.load_people!
150
- people_map = all_people.to_h { |p| [p.handle, p] }
151
151
  all_logs = @storage.all_days
152
152
 
153
153
  if person
154
- unless people_map.key?(person)
154
+ unless @people.key?(person)
155
155
  WorkLogger.error Rainbow("No person found with handle #{person}.").red
156
156
  return
157
157
  end
158
- person_detail(all_logs, all_people, people_map[person.strip])
158
+ person_detail(all_logs, @people, @people[person.strip])
159
159
  else
160
160
  puts 'People mentioned in the work log:'
161
161
 
@@ -169,8 +169,8 @@ module Worklog
169
169
  mentions = mentions.to_a.sort_by { |handle, _| handle }
170
170
 
171
171
  mentions.each do |handle, v|
172
- if people_map.key?(handle)
173
- person = people_map[handle]
172
+ if @people.key?(handle)
173
+ person = @people[handle]
174
174
  print "#{Rainbow(person.name).gold} (#{handle})"
175
175
  print " (#{person.team})" if person.team
176
176
  else
@@ -203,13 +203,43 @@ module Worklog
203
203
  def projects(_options = {})
204
204
  project_storage = ProjectStorage.new(@config)
205
205
  projects = project_storage.load_projects
206
- puts Rainbow('Projects:').gold
206
+
207
+ # Load all entries to find latest activity for each project
208
+ @storage.all_days.each do |daily_log|
209
+ daily_log.entries.each do |entry|
210
+ if projects.key?(entry.project)
211
+ project = projects[entry.project]
212
+ project.entries ||= []
213
+ project.entries << entry
214
+ # Update last activity date if entry time is more recent
215
+ project.last_activity = entry.time if project.last_activity.nil? || entry.time > project.last_activity
216
+ else
217
+ WorkLogger.debug "Project with key '#{entry.project}' not found in projects. Skipping."
218
+ end
219
+ end
220
+ end
221
+ print_projects(projects)
222
+ end
223
+
224
+ def print_projects(projects)
225
+ puts Rainbow('Active Projects:').gold
207
226
  projects.each_value do |project|
227
+ # Sort entries by descending time
228
+ project.entries.sort_by!(&:time).reverse!
229
+
208
230
  puts "#{Rainbow(project.name).gold} (#{project.key})"
209
231
  puts " Description: #{project.description}" if project.description
210
232
  puts " Start date: #{project.start_date.strftime('%b %d, %Y')}" if project.start_date
211
233
  puts " End date: #{project.end_date.strftime('%b %d, %Y')}" if project.end_date
212
234
  puts " Status: #{project.status}" if project.status
235
+ puts " Last activity: #{project.last_activity.strftime('%b %d, %Y')}" if project.last_activity
236
+
237
+ next unless project.entries && !project.entries.empty?
238
+
239
+ puts " Last #{[project.entries&.size, 3].min} entries:"
240
+ puts " #{project.entries.last(3).map do |e|
241
+ "#{e.time.strftime('%b %d, %Y')} #{e.message_string(@people)}"
242
+ end.join("\n ")}"
213
243
  end
214
244
  puts 'No projects found.' if projects.empty?
215
245
  end
@@ -254,7 +284,7 @@ module Worklog
254
284
  # @example
255
285
  # worklog.tag_detail('example_tag', from: '2023-10-01', to: '2023-10-31')
256
286
  def tag_detail(tag, options)
257
- printer = Printer.new(@storage.load_people!)
287
+ printer = Printer.new(@people)
258
288
  start_date, end_date = start_end_date(options)
259
289
 
260
290
  @storage.days_between(start_date, end_date).each do |daily_log|
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fewald-worklog
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.19
4
+ version: 0.2.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Friedrich Ewald