Name some colors
[infodrom.org/service.infodrom.org] / src / InfoCon / stempel / stempel
1 #! /usr/bin/perl
2
3 #  InfoCon Time Tracker
4 #  Copyright (c) 2007,8,13,14,15,20  Joey Schulze <joey@infodrom.org>
5 #
6 #  This program is free software; you can redistribute it and/or modify
7 #  it under the terms of the GNU General Public License as published by
8 #  the Free Software Foundation; either version 2 of the License, or
9 #  (at your option) any later version.
10 #
11 #  This program is distributed in the hope that it will be useful,
12 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
13 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 #  GNU General Public License for more details.
15 #
16 #  You should have received a copy of the GNU General Public License
17 #  along with this program; if not, write to the Free Software
18 #  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111, USA.
19
20 use strict;
21 use warnings;
22
23 use DBI;
24 use Date::Parse;
25 use Getopt::Long;
26 use Term::ReadLine;
27 use Term::ANSIColor;
28
29 my $table = "stempel";
30 my $engine = "dbi:Pg:dbname=infocon";
31
32 my $dbh = DBI->connect($engine);
33 die "Access to database denied!\n" unless $dbh;
34 $dbh->do("SET DateStyle = 'ISO'");
35
36 sub min2hour
37 {
38     my $minutes = shift;
39
40     return sprintf('%02d:%02d', $minutes/60, $minutes%60);
41 }
42
43 sub is_open
44 {
45     my $query = q{SELECT count(*) AS count FROM stempel WHERE stop IS NULL};
46     my $sth = $dbh->prepare ($query);
47     if ($sth && $sth->execute > 0) {
48         my $row = $sth->fetchrow_hashref;
49         return $row->{count} > 0 ? 1 : 0;
50     }
51     return 0;
52 }
53
54 sub customerlist
55 {
56     my @res;
57
58     my $query = q{SELECT DISTINCT customer FROM stempel WHERE start > now() - interval '1 year' ORDER BY customer};
59     my $sth = $dbh->prepare ($query);
60     if ($sth && $sth->execute > 0) {
61         while ((my $row = $sth->fetchrow_hashref)) {
62             push @res, $row->{customer};
63         }
64     }
65     return \@res;
66 }
67
68 sub tasklist
69 {
70     my $customer = shift;
71     my @res;
72
73     my $query = q{SELECT DISTINCT task FROM stempel WHERE start > now() - interval '1 month' AND customer = ? ORDER BY task};
74     my $sth = $dbh->prepare($query);
75     if ($sth && $sth->execute($customer) > 0) {
76         while ((my $row = $sth->fetchrow_hashref)) {
77             push @res, $row->{task};
78         }
79     }
80     return \@res;
81 }
82
83 sub complete_customer
84 {
85     my ($customer, $text, $line, $start) = @_;
86
87     # return () unless exists $answers->{category} && length $answers->{category};
88
89     my $sql = sprintf("SELECT DISTINCT task FROM stempel WHERE start > now() - interval '1 month' AND customer = '%s' AND task LIKE '%s%%' ORDER BY task",
90                       $customer,
91                       $line);
92
93     my $sth = $dbh->prepare($sql);
94     $sth->execute;
95     my @complete;
96     while (my $row = $sth->fetchrow_hashref) {
97         $row->{task} = substr $row->{task}, $start if $start;
98         push @complete, $row->{task};
99     }
100
101     return @complete;
102 }
103
104 sub open_task
105 {
106     my $term = new Term::ReadLine '';
107     my $customers = customerlist;
108
109     my $attribs = $term->Attribs;
110     $attribs->{completion_entry_function} = $attribs->{list_completion_function};
111     $attribs->{completion_word} = $customers;
112
113     printf "[%s]\n", join (', ', @$customers);
114
115     my $customer = $term->readline ('Kunde: ');
116
117     if (defined $customer && length($customer) > 0) {
118         $customer =~ s/\s*$//;
119
120         my $tasks = tasklist $customer;
121
122         $term->addhistory($_) foreach (@$tasks);
123         $attribs->{completion_word} = $tasks;
124         $attribs->{completion_entry_function} = undef;
125         $term->{completion_function} = sub {return complete_customer $customer, @_};
126         while (1) {
127             my $task = $term->readline ('Aufgabe: ');
128             return unless length $task;
129             return if $task eq 'q';
130
131             if ($task eq '?') {
132                 printf "  %s\n", $_ foreach (@$tasks);
133                 next;
134             }
135
136             $task =~ s/\s*$//;
137             my $query = q{INSERT INTO stempel (start,customer,task) VALUES('now()',?,?)};
138             my $sth = $dbh->prepare($query);
139             $sth->execute($customer, $task);
140             last;
141         }
142     }
143 }
144
145 sub quarter
146 {
147     my $min = shift;
148
149     return 15 if $min > 0 && $min < 15;
150
151     return $min - $min%15 if ($min%15 < 3);
152
153     return $min + 15-$min%15;
154 }
155
156 sub hdiff
157 {
158     my ($sh, $sm, $eh, $em) = @_;
159
160     if ($em >= $sm) {
161         return ($eh-$sh)*60 + $em-$sm;
162     } else {
163         return ($eh-$sh)*60 - ($sm-$em);
164     }
165 }
166
167 sub close_task
168 {
169     my $really = shift;
170     my ($d_sec,$d_min,$d_hour,$d_mday,$d_mon,$d_year,$d_wday,$d_isdst) = localtime();
171     my $today = 1;
172
173     my $query = q{SELECT oid,start FROM stempel WHERE stop IS NULL};
174     my $sth = $dbh->prepare ($query);
175     if ($sth && $sth->execute > 0) {
176         if ((my $row = $sth->fetchrow_hashref)) {
177             my @arr = split(/ /, $row->{start});
178             my @d = split(/-/, $arr[0]);
179
180             if ($d[0] != $d_year+1900 || $d[1] != $d_mon+1 || $d[2] != $d_mday) {
181                 if ($really) {
182                     printf "Task not started today.\n";
183                     $today = 0;
184                 } else {
185                     printf "Task not started today, aborting, use -D if this is intentional.\n";
186                     exit;
187                 }
188             }
189
190             my @t = split(/:/, $arr[1]);
191             my $int = quarter(hdiff($t[0], $t[1], $d_hour, $d_min));
192             $int += 24 * 60 unless $today;
193
194             $query = sprintf('UPDATE stempel SET stop=now(),time=%d WHERE oid = %d',
195                              $int, $row->{oid});
196             $dbh->do ($query);
197             printf "Wrote %s.\n", min2hour($int);
198         }
199     }
200     exit(0);
201 }
202
203 sub delete_task
204 {
205     my $query = q{DELETE FROM stempel WHERE stop IS NULL};
206     $dbh->do ($query);
207
208     exit(0);
209 }
210
211 sub reopen_task
212 {
213     my $sql = q{SELECT max(id) AS max_id FROM stempel};
214     my $sth = $dbh->prepare($sql);
215     if ($sth && $sth->execute > 0) {
216         my $row = $sth->fetchrow_hashref;
217         $sql = sprintf("UPDATE stempel SET stop = NULL, time = NULL WHERE id = %d", $row->{max_id});
218         $dbh->do($sql);
219     }
220
221     exit(0);
222 }
223
224 sub list_open
225 {
226     my $exit = shift;
227     my ($d_sec,$d_min,$d_hour,$d_mday,$d_mon,$d_year,$d_wday,$d_isdst) = localtime();
228     my $query = q{SELECT customer,start,task FROM stempel WHERE time IS NULL ORDER BY start,customer};
229     my $today = 1;
230
231     my $sth = $dbh->prepare ($query);
232     if ($sth && $sth->execute > 0) {
233         while ((my $row = $sth->fetchrow_hashref)) {
234             my @arr = split(/ /, $row->{start});
235             my $day = $arr[0];
236             my @d = split(/-/, $day);
237
238             if ($d[0] != $d_year+1900 || $d[1] != $d_mon+1 || $d[2] != $d_mday) {
239                 printf "Task not started today.\n";
240                 $today = 0;
241                 next if defined $exit && $exit eq 1;
242             }
243
244             my @t = split(/:/, $arr[1]);
245             my $time = sprintf('%02d:%02d', $t[0], $t[1]);
246             my $int = quarter(hdiff($t[0], $t[1], $d_hour, $d_min));
247             $int += 24 * 60 unless $today;
248
249             my $timediff = (str2time($row->{start}) - ( time() - ($int*60) )) / 60;
250             printf "%-15s  %s %s%s (%s)  %s\n", $row->{customer}, $day, $time,
251                 $timediff > 1 ? color('green').sprintf("[+%d]", $timediff).color('reset') : '',
252                 min2hour($int), $row->{task};
253         }
254     }
255     exit 0 if $exit;
256 }
257
258 sub list
259 {
260     my $month = shift;
261     my $verbose = shift;
262     my ($d_sec,$d_min,$d_hour,$d_mday,$d_mon,$d_year,$d_wday,$d_isdst) = localtime();
263     my $query = q{SELECT start::date,to_char(start, 'HH24:MI') AS starttime,customer,time,task FROM stempel WHERE time IS NOT NULL };
264     my $display_sum = 0;
265     my $sum = 0;
266     
267     if ($month =~ /^(\d{4})-?(\d\d?)$/) {
268         my $pivot = $1 . '-' . $2 . '-01';
269         $query .= "AND start >= '$pivot' AND start <= '$pivot'::date + interval '1 month' ";
270     } elsif ($month =~ /^(\d{4})$/) {
271         my $pivot = $1 . '-01-01';
272         $query .= "AND start >= '$pivot' AND start <= '$pivot'::date + interval '1 year' ";
273     } elsif ($month =~ /^(\d\d?)$/) {
274         my $pivot = $d_year+1900 . '-' . $1 . '-01';
275         $query .= "AND start >= '$pivot' AND start <= '$pivot'::date + interval '1 month' ";
276     } elsif (length $month && $month eq 'today') {
277         $query .= "AND start::date = now()::date ";
278         $display_sum = 1;
279     } elsif (length $month && $month =~ /^yester(day)?$/) {
280         $query .= "AND start::date = (now() - interval '1 day')::date ";
281         $display_sum = 1;
282     } elsif (length $month && $month !~ /^(all|current)$/) {
283         $query .= "AND task LIKE '%${month}%' ";
284         $display_sum = 1;
285     } elsif ($month !~ /^all$/ || $month eq 'current') {
286         my $pivot = sprintf('%04d-%02d-01', $d_year+1900, $d_mon+1);
287         $query .= "AND start >= '$pivot' AND start <= '$pivot'::date + interval '1 month' ";
288     }
289     $query .= "ORDER BY customer,start,starttime";
290
291     my $sth = $dbh->prepare ($query);
292
293     my $customer = '';
294     my @colors = qw/green yellow red cyan magenta blue/;
295     my $color = $#colors;
296     if ($sth && $sth->execute > 0) {
297         while ((my $row = $sth->fetchrow_hashref)) {
298             if (defined $row->{time}) {
299                 if ($customer ne $row->{customer}) {
300                     $customer = $row->{customer};
301                     if ($color == $#colors) {
302                         $color = 0;
303                     } else {
304                         $color++;
305                     }
306                 }
307                 $sum += $row->{time};
308                 my $time = min2hour $row->{time};
309                 print color($colors[$color]) if -t STDOUT;
310                 printf "%-15s  %s  %s  %s", $row->{customer}, $row->{start}.($verbose ? ' '.$row->{starttime} : ''), $time, $row->{task};
311                 print color('reset') if -t STDOUT;
312                 print "\n";
313             }
314         }
315     }
316
317     if ($display_sum) {
318         printf "%-27s  %s\n", "Summe", min2hour $sum;
319     }
320
321     exit 0;
322 }
323
324 sub toggle_task
325 {
326     if (is_open) {
327         list_open 1;
328     } else {
329         open_task;
330     }
331 }
332
333 sub late_close_task
334 {
335     my $delta = shift;
336     my ($d_sec,$d_min,$d_hour,$d_mday,$d_mon,$d_year,$d_wday,$d_isdst) = localtime();
337
338     return unless is_open;
339
340     my $query = sprintf("UPDATE stempel SET stop=start + interval '%d minutes',time=%d WHERE stop IS NULL",
341                         $delta, quarter($delta));
342     $dbh->do($query);
343 }
344
345 sub move_task
346 {
347     my $minutes = shift;
348
349     return unless is_open;
350
351     my $query = sprintf("UPDATE stempel SET start = start - interval '%d minutes' WHERE stop IS NULL", $minutes);
352     $dbh->do($query);
353 }
354
355 sub alter_task
356 {
357     return unless is_open;
358
359     list_open;
360
361     my $term = new Term::ReadLine '';
362
363     my $sql = q{SELECT customer,task FROM stempel WHERE stop IS NULL};
364     my $sth = $dbh->prepare($sql);
365     $sth->execute;
366     my $row = $sth->fetchrow_hashref;
367
368     my $tasks = tasklist $row->{'customer'};
369     $term->addhistory($_) foreach (@$tasks);
370
371     my $attribs = $term->Attribs;
372     $attribs->{completion_word} = $tasks;
373     $term->{completion_function} = sub {return complete_customer $row->{'customer'}, @_};
374
375     my $task = $term->readline ('Aufgabe: ');
376
377     if (length($task) > 0) {
378         my $query = q{UPDATE stempel SET task=? WHERE stop IS NULL};
379         my $sth = $dbh->prepare($query);
380         $sth->execute($task);
381     }
382     exit 0;
383 }
384
385 sub help
386 {
387     print <<"END";
388 stempel  Copyright (c) 2007,8  Martin Schulze <joey\@infodrom.org>
389     --list [month] list month [default=current|all|yesterday|today]
390     --back n       move start of current task back by n minutes
391     --open         list open
392     --close time   close open task
393     --help         this text
394     --end|-d       terminate task
395     -D             terminate task not started today
396     --delete       delete open task
397     --reopen       re-open last task
398     --task         alter task content
399 END
400     exit;
401 }
402
403 my $opt_close = undef;
404 my $opt_list = undef;
405 my $opt_back = undef;
406 my $opt_verbose = undef;
407 my %options = ('list:s' => \$opt_list,
408                'back=i' => \$opt_back,
409                'open' => \&list_open,
410                'help' => \&help,
411                'close=i' => \$opt_close,
412                'delete' => \&delete_task,
413                'reopen' => \&reopen_task,
414                'task|t' => \&alter_task,
415                'terminate|end|d' => sub {close_task(0);},
416                'D' => sub {close_task(1);},
417                'verbose' => \$opt_verbose,
418                );
419 Getopt::Long::Configure('no_ignore_case');
420 GetOptions %options;
421
422 if ($opt_close) {
423     late_close_task $opt_close;
424 } elsif (defined $opt_list) {
425     list $opt_list, $opt_verbose;
426 } elsif (defined $opt_back) {
427     move_task $opt_back;
428 } else {
429     toggle_task;
430 }