File: | lib/App/TimeTracker/Command/Core.pm |
Coverage: | 17.2% |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package App::TimeTracker::Command::Core; | ||||||
2 | 2 2 2 | 352704 6 43 | use strict; | ||||
3 | 2 2 2 | 11 5 62 | use warnings; | ||||
4 | 2 2 2 2 2 2 | 110 103 31 8 6 24 | use 5.010; | ||||
5 | |||||||
6 | # ABSTRACT: TimeTracker Core commands | ||||||
7 | |||||||
8 | 2 2 2 | 11 5 27 | use Moose::Role; | ||||
9 | 2 2 2 | 6722 1619 107 | use File::Copy qw(move); | ||||
10 | 2 2 2 | 672 9477 19 | use File::Find::Rule; | ||||
11 | |||||||
12 | sub cmd_start { | ||||||
13 | 0 | 0 | 0 | my $self = shift; | |||
14 | |||||||
15 | 0 | 0 | $self->cmd_stop; | ||||
16 | |||||||
17 | 0 | 0 | my $task = App::TimeTracker::Data::Task->new({ | ||||
18 | start=>$self->at || $self->now, | ||||||
19 | project=>$self->project, | ||||||
20 | tags=>$self->tags, | ||||||
21 | }); | ||||||
22 | 0 | 0 | $self->_current_task($task); | ||||
23 | |||||||
24 | 0 | 0 | $task->do_start($self->home); | ||||
25 | } | ||||||
26 | |||||||
27 | sub cmd_stop { | ||||||
28 | 0 | 0 | 0 | my $self = shift; | |||
29 | |||||||
30 | 0 | 0 | my $task = App::TimeTracker::Data::Task->current($self->home); | ||||
31 | 0 | 0 | return unless $task; | ||||
32 | 0 | 0 | $self->_previous_task($task); | ||||
33 | |||||||
34 | 0 | 0 | $task->stop($self->at || $self->now); | ||||
35 | 0 | 0 | $task->save($self->home); | ||||
36 | |||||||
37 | 0 | 0 | move($self->home->file('current')->stringify,$self->home->file('previous')->stringify); | ||||
38 | |||||||
39 | 0 | 0 | say "Worked ".$task->duration." on ".$task->say_project_tags; | ||||
40 | } | ||||||
41 | |||||||
42 | sub cmd_current { | ||||||
43 | 0 | 0 | 0 | my $self = shift; | |||
44 | |||||||
45 | 0 | 0 | if (my $task = App::TimeTracker::Data::Task->current($self->home)) { | ||||
46 | 0 | 0 | say "Working ".$task->_calc_duration($self->now)." on ".$task->say_project_tags; | ||||
47 | } | ||||||
48 | elsif (my $prev = App::TimeTracker::Data::Task->previous($self->home)) { | ||||||
49 | 0 | 0 | say "Currently not working on anything, but the last thing you worked on was:"; | ||||
50 | 0 | 0 | say $prev->say_project_tags; | ||||
51 | } | ||||||
52 | else { | ||||||
53 | 0 | 0 | say "Currently not working on anything, and I have no idea what you worked on earlier..."; | ||||
54 | } | ||||||
55 | } | ||||||
56 | |||||||
57 | sub cmd_append { | ||||||
58 | 0 | 0 | 0 | my $self = shift; | |||
59 | |||||||
60 | 0 | 0 | if (my $task = App::TimeTracker::Data::Task->current($self->home)) { | ||||
61 | 0 | 0 | say "Cannot 'append', you're actually already working on :" | ||||
62 | . $task->say_project_tags . "\n"; | ||||||
63 | } | ||||||
64 | elsif (my $prev = App::TimeTracker::Data::Task->previous($self->home)) { | ||||||
65 | |||||||
66 | 0 | 0 | my $task = App::TimeTracker::Data::Task->new({ | ||||
67 | start=>$prev->stop, | ||||||
68 | project => $self->project, | ||||||
69 | tags=>$self->tags, | ||||||
70 | }); | ||||||
71 | 0 | 0 | $self->_current_task($task); | ||||
72 | 0 | 0 | $task->do_start($self->home); | ||||
73 | } | ||||||
74 | else { | ||||||
75 | 0 | 0 | say "Currently not working on anything and I have no idea what you've been doing."; | ||||
76 | } | ||||||
77 | } | ||||||
78 | |||||||
79 | sub cmd_continue { | ||||||
80 | 0 | 0 | 0 | my $self = shift; | |||
81 | |||||||
82 | 0 | 0 | if (my $task = App::TimeTracker::Data::Task->current($self->home)) { | ||||
83 | 0 | 0 | say "Cannot 'continue', you're working on something:\n".$task->say_project_tags; | ||||
84 | } | ||||||
85 | elsif (my $prev = App::TimeTracker::Data::Task->previous($self->home)) { | ||||||
86 | 0 | 0 | my $task = App::TimeTracker::Data::Task->new({ | ||||
87 | start=>$self->at || $self->now, | ||||||
88 | project=>$prev->project, | ||||||
89 | tags=>$prev->tags, | ||||||
90 | }); | ||||||
91 | 0 | 0 | $self->_current_task($task); | ||||
92 | 0 | 0 | $task->do_start($self->home); | ||||
93 | } | ||||||
94 | else { | ||||||
95 | 0 | 0 | say "Currently not working on anything, and I have no idea what you worked on earlier..."; | ||||
96 | } | ||||||
97 | } | ||||||
98 | |||||||
99 | sub cmd_worked { | ||||||
100 | 0 | 0 | 0 | my $self = shift; | |||
101 | |||||||
102 | 0 | 0 | my @files = $self->find_task_files({ | ||||
103 | from=>$self->from, | ||||||
104 | to=>$self->to, | ||||||
105 | projects=>$self->projects, | ||||||
106 | }); | ||||||
107 | |||||||
108 | 0 | 0 | my $total=0; | ||||
109 | 0 | 0 | foreach my $file ( @files ) { | ||||
110 | 0 | 0 | my $task = App::TimeTracker::Data::Task->load($file->stringify); | ||||
111 | 0 | 0 | $total+=$task->seconds // $task->_build_seconds; | ||||
112 | } | ||||||
113 | |||||||
114 | 0 | 0 | say $self->beautify_seconds($total); | ||||
115 | } | ||||||
116 | |||||||
117 | sub cmd_report { | ||||||
118 | 0 | 0 | 0 | my $self = shift; | |||
119 | |||||||
120 | 0 | 0 | my @files = $self->find_task_files({ | ||||
121 | from=>$self->from, | ||||||
122 | to=>$self->to, | ||||||
123 | projects=>$self->projects, | ||||||
124 | }); | ||||||
125 | |||||||
126 | 0 | 0 | my $total = 0; | ||||
127 | 0 | 0 | my $report={}; | ||||
128 | 0 | 0 | my $format="%- 20s % 12s\n"; | ||||
129 | |||||||
130 | 0 | 0 | my $job_map = $self->config->{project2job}; | ||||
131 | |||||||
132 | 0 | 0 | foreach my $file ( @files ) { | ||||
133 | 0 | 0 | my $task = App::TimeTracker::Data::Task->load($file->stringify); | ||||
134 | 0 | 0 | my $time = $task->seconds // $task->_build_seconds; | ||||
135 | 0 | 0 | my $project = $task->project; | ||||
136 | 0 | 0 | my $job = $job_map->{$project} || '_nojob'; | ||||
137 | |||||||
138 | 0 | 0 | if ($time >= 60*60*8) { | ||||
139 | 0 | 0 | say "Found dubious trackfile: ".$file->basename; | ||||
140 | 0 | 0 | say " Are you sure you worked ".$self->beautify_seconds($time)." on one task?"; | ||||
141 | } | ||||||
142 | |||||||
143 | 0 | 0 | $total+=$time; | ||||
144 | |||||||
145 | 0 | 0 | $report->{$job}{'_total'} += $time; | ||||
146 | 0 | 0 | $report->{$job}{$project}{'_total'} += $time; | ||||
147 | |||||||
148 | 0 | 0 | if ( $self->detail ) { | ||||
149 | 0 | 0 | my $tags = $task->tags; | ||||
150 | 0 | 0 | if (@$tags) { | ||||
151 | 0 | 0 | foreach my $tag ( @$tags ) { | ||||
152 | 0 | 0 | $report->{$job}{$project}{$tag} += $time; | ||||
153 | } | ||||||
154 | } | ||||||
155 | else { | ||||||
156 | 0 | 0 | $report->{$job}{$project}{'_untagged'} += $time; | ||||
157 | } | ||||||
158 | } | ||||||
159 | 0 | 0 | if ($self->verbose) { | ||||
160 | 0 | 0 | printf("%- 40s -> % 8s\n",$file->basename, $self->beautify_seconds($time)); | ||||
161 | } | ||||||
162 | } | ||||||
163 | |||||||
164 | 0 | 0 | my $padding=''; | ||||
165 | 0 | 0 | my $tagpadding=' '; | ||||
166 | 0 | 0 | foreach my $job (sort keys %$report) { | ||||
167 | 0 | 0 | my $job_total = delete $report->{$job}{'_total'}; | ||||
168 | 0 | 0 | unless ($job eq '_nojob') { | ||||
169 | 0 | 0 | printf ($format, $job, $self->beautify_seconds( $job_total ) ); | ||||
170 | 0 | 0 | $padding = " "; | ||||
171 | } | ||||||
172 | |||||||
173 | 0 0 | 0 0 | foreach my $project (sort keys %{$report->{$job}}) { | ||||
174 | 0 | 0 | my $data = $report->{$job}{$project}; | ||||
175 | 0 | 0 | printf( $padding.$format, $project, $self->beautify_seconds( delete $data->{'_total'} ) ); | ||||
176 | 0 | 0 | printf( $padding.$tagpadding.$format, 'untagged', $self->beautify_seconds( delete $data->{'_untagged'} ) ) if $data->{'_untagged'}; | ||||
177 | |||||||
178 | 0 | 0 | if ( $self->detail ) { | ||||
179 | 0 0 0 | 0 0 0 | foreach my $tag ( sort { $data->{$b} <=> $data->{$a} } keys %{ $data } ) { | ||||
180 | 0 | 0 | my $time = $data->{$tag}; | ||||
181 | 0 | 0 | printf( $padding.$tagpadding.$format, $tag, $self->beautify_seconds($time) ); | ||||
182 | } | ||||||
183 | } | ||||||
184 | } | ||||||
185 | } | ||||||
186 | #say '=' x 35; | ||||||
187 | 0 | 0 | printf( $format, 'total', $self->beautify_seconds($total) ); | ||||
188 | } | ||||||
189 | |||||||
190 | sub cmd_recalc_trackfile { | ||||||
191 | 0 | 0 | 0 | my $self = shift; | |||
192 | 0 | 0 | my $file = $self->trackfile; | ||||
193 | 0 | 0 | unless (-e $file) { | ||||
194 | 0 | 0 | $file =~ /(?<year>\d\d\d\d)(?<month>\d\d)\d\d-\d{6}_\w+\.trc/; | ||||
195 | 0 | 0 | if ($+{year} && $+{month}) { | ||||
196 | 0 | 0 | $file = $self->home->file($+{year},$+{month},$file)->stringify; | ||||
197 | 0 | 0 | unless (-e $file) { | ||||
198 | 0 | 0 | say "Cannot find file ".$self->trackfile; | ||||
199 | 0 | 0 | exit; | ||||
200 | } | ||||||
201 | } | ||||||
202 | } | ||||||
203 | |||||||
204 | 0 | 0 | my $task = App::TimeTracker::Data::Task->load($file); | ||||
205 | 0 | 0 | $task->save($self->home); | ||||
206 | 0 | 0 | say "recalced $file"; | ||||
207 | } | ||||||
208 | |||||||
209 | sub cmd_commands { | ||||||
210 | 1 | 0 | 3 | my $self = shift; | |||
211 | |||||||
212 | 1 | 50 | say "Available commands:"; | ||||
213 | 1 | 11 | foreach my $method ($self->meta->get_all_method_names) { | ||||
214 | 69 | 3377 | next unless $method =~ /^cmd_/; | ||||
215 | 11 | 29 | $method =~ s/^cmd_//; | ||||
216 | 11 | 71 | say "\t$method"; | ||||
217 | } | ||||||
218 | 1 | 244 | exit; | ||||
219 | } | ||||||
220 | |||||||
221 | sub _load_attribs_worked { | ||||||
222 | 0 | my ($class, $meta) = @_; | |||||
223 | 0 | $meta->add_attribute('from'=>{ | |||||
224 | isa=>'TT::DateTime', | ||||||
225 | is=>'ro', | ||||||
226 | coerce=>1, | ||||||
227 | lazy_build=>1, | ||||||
228 | }); | ||||||
229 | 0 | $meta->add_attribute('to'=>{ | |||||
230 | isa=>'TT::DateTime', | ||||||
231 | is=>'ro', | ||||||
232 | coerce=>1, | ||||||
233 | lazy_build=>1, | ||||||
234 | }); | ||||||
235 | 0 | $meta->add_attribute('this'=>{ | |||||
236 | isa=>'Str', | ||||||
237 | is=>'ro', | ||||||
238 | }); | ||||||
239 | 0 | $meta->add_attribute('last'=>{ | |||||
240 | isa=>'Str', | ||||||
241 | is=>'ro', | ||||||
242 | }); | ||||||
243 | 0 | $meta->add_attribute('projects'=>{ | |||||
244 | isa=>'ArrayRef[Str]', | ||||||
245 | is=>'ro', | ||||||
246 | }); | ||||||
247 | } | ||||||
248 | sub _load_attribs_report { | ||||||
249 | 0 | my ($class, $meta) = @_; | |||||
250 | 0 | $class->_load_attribs_worked($meta); | |||||
251 | 0 | $meta->add_attribute('detail'=>{ | |||||
252 | isa=>'Bool', | ||||||
253 | is=>'ro', | ||||||
254 | }); | ||||||
255 | 0 | $meta->add_attribute('verbose'=>{ | |||||
256 | isa=>'Bool', | ||||||
257 | is=>'ro', | ||||||
258 | }); | ||||||
259 | } | ||||||
260 | |||||||
261 | sub _load_attribs_start { | ||||||
262 | 0 | my ($class, $meta) = @_; | |||||
263 | 0 | $meta->add_attribute('at'=>{ | |||||
264 | isa=>'TT::DateTime', | ||||||
265 | is=>'ro', | ||||||
266 | coerce=>1, | ||||||
267 | }); | ||||||
268 | 0 | $meta->add_attribute('project'=>{ | |||||
269 | isa=>'Str', | ||||||
270 | is=>'ro', | ||||||
271 | lazy_build=>1, | ||||||
272 | }); | ||||||
273 | } | ||||||
274 | *_load_attribs_append = \&_load_attribs_start; | ||||||
275 | *_load_attribs_continue = \&_load_attribs_start; | ||||||
276 | *_load_attribs_stop = \&_load_attribs_start; | ||||||
277 | |||||||
278 | sub _load_attribs_recalc_trackfile { | ||||||
279 | 0 | my ($class, $meta) = @_; | |||||
280 | 0 | $meta->add_attribute('trackfile'=>{ | |||||
281 | isa=>'Str', | ||||||
282 | is=>'ro', | ||||||
283 | required=>1, | ||||||
284 | }); | ||||||
285 | } | ||||||
286 | |||||||
287 | sub _build_from { | ||||||
288 | 0 | my $self = shift; | |||||
289 | 0 | if (my $last = $self->last) { | |||||
290 | 0 | return $self->now->truncate( to => $last)->subtract( $last.'s' => 1 ); | |||||
291 | } | ||||||
292 | elsif (my $this = $self->this) { | ||||||
293 | 0 | return $self->now->truncate( to => $this); | |||||
294 | } | ||||||
295 | } | ||||||
296 | |||||||
297 | sub _build_to { | ||||||
298 | 0 | my $self = shift; | |||||
299 | 0 | my $dur = $self->this || $self->last; | |||||
300 | 0 | return $self->from->clone->add( $dur.'s' => 1 ); | |||||
301 | } | ||||||
302 | |||||||
303 | 2 2 2 | 2804 6 21 | no Moose::Role; | ||||
304 | 1; | ||||||
305 |