Allow closing tasks not started today with -D
[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  Martin 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 Getopt::Long;
25 use Term::ReadLine;
26
27 my $table = "stempel";
28 my $engine = "dbi:Pg:dbname=infocon";
29
30 my $dbh = DBI->connect($engine);
31 die "Access to database denied!\n" unless $dbh;
32 $dbh->do("SET DateStyle = 'ISO'");
33
34 sub min2hour
35 {
36     my $minutes = shift;
37
38     return sprintf('%02d:%02d', $minutes/60, $minutes%60);
39 }
40
41 sub is_open
42 {
43     my $query = q{SELECT count(*) AS count FROM stempel WHERE stop IS NULL};
44     my $sth = $dbh->prepare ($query);
45     if ($sth && $sth->execute > 0) {
46         my $row = $sth->fetchrow_hashref;
47         return $row->{count} > 0 ? 1 : 0;
48     }
49     return 0;
50 }
51
52 sub customerlist
53 {
54     my @res;
55
56     my $query = q{SELECT DISTINCT customer FROM stempel WHERE start > now() - interval '1 year' ORDER BY customer};
57     my $sth = $dbh->prepare ($query);
58     if ($sth && $sth->execute > 0) {
59         while ((my $row = $sth->fetchrow_hashref)) {
60             push @res, $row->{customer};
61         }
62     }
63     return \@res;
64 }
65
66 sub tasklist
67 {
68     my $customer = shift;
69     my @res;
70
71     my $query = q{SELECT DISTINCT task FROM stempel WHERE start > now() - interval '1 month' AND customer = ? ORDER BY task};
72     my $sth = $dbh->prepare($query);
73     if ($sth && $sth->execute($customer) > 0) {
74         while ((my $row = $sth->fetchrow_hashref)) {
75             push @res, $row->{task};
76         }
77     }
78     return \@res;
79 }
80
81 sub complete_customer
82 {
83     my ($customer, $text, $line, $start) = @_;
84
85     # return () unless exists $answers->{category} && length $answers->{category};
86
87     my $sql = sprintf("SELECT DISTINCT task FROM stempel WHERE start > now() - interval '1 month' AND customer = '%s' AND task LIKE '%s%%' ORDER BY task",
88                       $customer,
89                       $line);
90
91     my $sth = $dbh->prepare($sql);
92     $sth->execute;
93     my @complete;
94     while (my $row = $sth->fetchrow_hashref) {
95         $row->{task} = substr $row->{task}, $start if $start;
96         push @complete, $row->{task};
97     }
98
99     return @complete;
100 }
101
102 sub open_task
103 {
104     my $term = new Term::ReadLine '';
105     my $customers = customerlist;
106
107     my $attribs = $term->Attribs;
108     $attribs->{completion_entry_function} = $attribs->{list_completion_function};
109     $attribs->{completion_word} = $customers;
110
111     printf "[%s]\n", join (', ', @$customers);
112
113     my $customer = $term->readline ('Kunde: ');
114
115     if (defined $customer && length($customer) > 0) {
116         $customer =~ s/\s*$//;
117
118         my $tasks = tasklist $customer;
119
120         $term->addhistory($_) foreach (@$tasks);
121         $attribs->{completion_word} = $tasks;
122         $attribs->{completion_entry_function} = undef;
123         $term->{completion_function} = sub {return complete_customer $customer, @_};
124         while (1) {
125             my $task = $term->readline ('Aufgabe: ');
126             return unless length $task;
127             return if $task eq 'q';
128
129             if ($task eq '?') {
130                 printf "  %s\n", $_ foreach (@$tasks);
131                 next;
132             }
133
134             $task =~ s/\s*$//;
135             my $query = q{INSERT INTO stempel (start,customer,task) VALUES('now()',?,?)};
136             my $sth = $dbh->prepare($query);
137             $sth->execute($customer, $task);
138             last;
139         }
140     }
141 }
142
143 sub quarter
144 {
145     my $min = shift;
146
147     return 15 if $min > 0 && $min < 15;
148
149     return $min - $min%15 if ($min%15 < 3);
150
151     return $min + 15-$min%15;
152 }
153
154 sub hdiff
155 {
156     my ($sh, $sm, $eh, $em) = @_;
157
158     if ($em >= $sm) {
159         return ($eh-$sh)*60 + $em-$sm;
160     } else {
161         return ($eh-$sh)*60 - ($sm-$em);
162     }
163 }
164
165 sub close_task
166 {
167     my $really = shift;
168     my ($d_sec,$d_min,$d_hour,$d_mday,$d_mon,$d_year,$d_wday,$d_isdst) = localtime();
169     my $today = 1;
170
171     my $query = q{SELECT oid,start FROM stempel WHERE stop IS NULL};
172     my $sth = $dbh->prepare ($query);
173     if ($sth && $sth->execute > 0) {
174         while ((my $row = $sth->fetchrow_hashref)) {
175             my @arr = split(/ /, $row->{start});
176             my @d = split(/-/, $arr[0]);
177
178             if ($d[0] != $d_year+1900 || $d[1] != $d_mon+1 || $d[2] != $d_mday) {
179                 if ($really) {
180                     printf "Task not started today.\n";
181                     $today = 0;
182                 } else {
183                     printf "Task not started today, aborting, use -D if this is intentional.\n";
184                     next;
185                 }
186             }
187
188             my @t = split(/:/, $arr[1]);
189             my $int = quarter(hdiff($t[0], $t[1], $d_hour, $d_min));
190             $int += 24 * 60 unless $today;
191
192             $query = sprintf('UPDATE stempel SET stop=now(),time=%d WHERE oid = %d',
193                              $int, $row->{oid});
194             $dbh->do ($query);
195             printf "Wrote %s.\n", min2hour($int);
196         }
197     }
198     exit(0);
199 }
200
201 sub delete_task
202 {
203     my $query = q{DELETE FROM stempel WHERE stop IS NULL};
204     $dbh->do ($query);
205
206     exit(0);
207 }
208
209 sub reopen_task
210 {
211     my $sql = q{SELECT max(id) AS max_id FROM stempel};
212     my $sth = $dbh->prepare($sql);
213     if ($sth && $sth->execute > 0) {
214         my $row = $sth->fetchrow_hashref;
215         $sql = sprintf("UPDATE stempel SET stop = NULL, time = NULL WHERE id = %d", $row->{max_id});
216         $dbh->do($sql);
217     }
218
219     exit(0);
220 }
221
222 sub list_open
223 {
224     my $exit = shift;
225     my ($d_sec,$d_min,$d_hour,$d_mday,$d_mon,$d_year,$d_wday,$d_isdst) = localtime();
226     my $query = q{SELECT customer,start,task FROM stempel WHERE time IS NULL ORDER BY start,customer};
227     my $today = 1;
228
229     my $sth = $dbh->prepare ($query);
230     if ($sth && $sth->execute > 0) {
231         while ((my $row = $sth->fetchrow_hashref)) {
232             my @arr = split(/ /, $row->{start});
233             my $day = $arr[0];
234             my @d = split(/-/, $day);
235
236             if ($d[0] != $d_year+1900 || $d[1] != $d_mon+1 || $d[2] != $d_mday) {
237                 printf "Task not started today.\n";
238                 $today = 0;
239                 next if defined $exit && $exit eq 1;
240             }
241
242             my @t = split(/:/, $arr[1]);
243             my $time = sprintf('%02d:%02d', $t[0], $t[1]);
244             my $int = quarter(hdiff($t[0], $t[1], $d_hour, $d_min));
245             $int += 24 * 60 unless $today;
246
247             printf "%-15s  %s %s (%s)  %s\n", $row->{customer}, $day, $time, min2hour($int), $row->{task};
248         }
249     }
250     exit 0 if $exit;
251 }
252
253 sub list
254 {
255     my $month = shift;
256     my ($d_sec,$d_min,$d_hour,$d_mday,$d_mon,$d_year,$d_wday,$d_isdst) = localtime();
257     my $query = q{SELECT start,customer,time,task FROM stempel WHERE time IS NOT NULL };
258     
259     if ($month =~ /^(\d{4})-?(\d\d?)$/) {
260         my $pivot = $1 . '-' . $2 . '-01';
261         $query .= "AND start >= '$pivot' AND start <= '$pivot'::date + interval '1 month' ";
262     } elsif ($month =~ /^(\d{4})$/) {
263         my $pivot = $1 . '-01-01';
264         $query .= "AND start >= '$pivot' AND start <= '$pivot'::date + interval '1 year' ";
265     } elsif ($month =~ /^(\d\d?)$/) {
266         my $pivot = $d_year+1900 . '-' . $1 . '-01';
267         $query .= "AND start >= '$pivot' AND start <= '$pivot'::date + interval '1 month' ";
268     } elsif ($month !~ /^all$/) {
269         my $pivot = sprintf('%04d-%02d-01', $d_year+1900, $d_mon+1);
270         $query .= "AND start >= '$pivot' AND start <= '$pivot'::date + interval '1 month' ";
271     }
272     $query .= "ORDER BY customer,start";
273
274     my $sth = $dbh->prepare ($query);
275
276     if ($sth && $sth->execute > 0) {
277         while ((my $row = $sth->fetchrow_hashref)) {
278             if (defined $row->{time}) {
279                 my $day = (split(/ /, $row->{start}))[0];
280                 my $time = min2hour $row->{time};
281                 printf "%-15s  %s  %s  %s\n", $row->{customer}, $day, $time, $row->{task};
282             }
283         }
284     }
285     exit 0;
286 }
287
288 sub toggle_task
289 {
290     if (is_open) {
291         list_open 1;
292     } else {
293         open_task;
294     }
295 }
296
297 sub late_close_task
298 {
299     my $delta = shift;
300     my ($d_sec,$d_min,$d_hour,$d_mday,$d_mon,$d_year,$d_wday,$d_isdst) = localtime();
301
302     return unless is_open;
303
304     my $query = sprintf("UPDATE stempel SET stop=start + interval '%d minutes',time=%d WHERE stop IS NULL",
305                         $delta, quarter($delta));
306     $dbh->do($query);
307 }
308
309 sub move_task
310 {
311     my $minutes = shift;
312
313     return unless is_open;
314
315     my $query = sprintf("UPDATE stempel SET start = start - interval '%d minutes' WHERE stop IS NULL", $minutes);
316     $dbh->do($query);
317 }
318
319 sub alter_task
320 {
321     return unless is_open;
322
323     list_open;
324
325     my $term = new Term::ReadLine '';
326
327     my $sql = q{SELECT customer,task FROM stempel WHERE stop IS NULL};
328     my $sth = $dbh->prepare($sql);
329     $sth->execute;
330     my $row = $sth->fetchrow_hashref;
331
332     my $tasks = tasklist $row->{'customer'};
333     $term->addhistory($_) foreach (@$tasks);
334
335     my $attribs = $term->Attribs;
336     $attribs->{completion_word} = $tasks;
337     $term->{completion_function} = sub {return complete_customer $row->{'customer'}, @_};
338
339     my $task = $term->readline ('Aufgabe: ');
340
341     if (length($task) > 0) {
342         my $query = q{UPDATE stempel SET task=? WHERE stop IS NULL};
343         my $sth = $dbh->prepare($query);
344         $sth->execute($task);
345     }
346     exit 0;
347 }
348
349 sub help
350 {
351     print <<"END";
352 stempel  Copyright (c) 2007,8  Martin Schulze <joey\@infodrom.org>
353     --list [month] list month [default=current|all]
354     --back n       move start of current task back by n minutes
355     --open         list open
356     --close time   close open task
357     --help         this text
358     --end|-d       terminate task
359     -D             terminate task not started today
360     --delete       delete open task
361     --reopen       re-open last task
362     --task         alter task content
363 END
364     exit;
365 }
366
367 my $opt_close = undef;
368 my $opt_list = undef;
369 my $opt_back = undef;
370 my %options = ('list:s' => \$opt_list,
371                'back=i' => \$opt_back,
372                'open' => \&list_open,
373                'help' => \&help,
374                'close=i' => \$opt_close,
375                'delete' => \&delete_task,
376                'reopen' => \&reopen_task,
377                'task|t' => \&alter_task,
378                'terminate|end|d' => \&close_task,
379                'D' => sub {close_task(1);},
380                );
381 GetOptions %options;
382
383 if ($opt_close) {
384     late_close_task $opt_close;
385 } elsif (defined $opt_list) {
386     list $opt_list;
387 } elsif (defined $opt_back) {
388     move_task $opt_back;
389 } else {
390     toggle_task;
391 }