| 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 | |||||||