File Coverage

File:lib/App/TimeTracker/Command/Core.pm
Coverage:17.2%

linestmtbrancondsubpodtimecode
1package 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
12sub 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
27sub 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
42sub 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
57sub 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
79sub 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
99sub 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
117sub 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
190sub 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
209sub 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
221sub _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}
248sub _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
261sub _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
278sub _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
287sub _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
297sub _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;
3041;
305