#!/usr/bin/perl use Glib qw/TRUE FALSE/; use Gtk2 '-init'; BEGIN { ($parentdir) = $0 =~ /^(.*\/)/; $parentdir ||= "." } use lib "$parentdir"; use SafeDBI; use DBI; require "$ENV{HOME}/.tomtracker"; my $dsn = "DBI:mysql:database=$config{DBNAME};host=$config{DBHOST}"; my $dbh = SafeDBI::new ($dsn, $config{DBUSER}, $config{DBPASS}); sub callback_save { my ($widget, $stuff) = @_; if ($stuff->{"hourly_rate"}->get_text() != $stuff->{"row"}->{"hourly_rate"}) { $dbh->do ("update iv set iv_hourly_rate=? where iv_id=?", undef, $stuff->{"hourly_rate"}->get_text(), $stuff->{"row"}->{"iv_id"}); $stuff->{"row"}->{"hourly_rate"} = 0 + $stuff->{"hourly_rate"}->get_text(); } if (length $stuff->{"iv_starttime"}->get_text()) { $dbh->do ("update iv set iv_starttime=? where iv_id=?", undef, $stuff->{"iv_starttime"}->get_text(), $stuff->{"row"}->{"iv_id"}); my $sth = $dbh->prepare (select_iv() . " where iv_id=?"); $sth->execute ($stuff->{"row"}->{"iv_id"}); my $row = $sth->fetchrow; $stuff->{"row"}->{"iv_starttimestamp"} = $row->{"iv_starttimestamp"}; } if (defined $stuff->{"row"}->{"iv_duration"} && ($stuff->{"row"}->{"iv_duration"} != hms2s ($stuff->{"iv_duration"}->get_text()))) { $dbh->do ("update iv set iv_duration=? where iv_id=?", undef, hms2s($stuff->{"iv_duration"}->get_text()), $stuff->{"row"}->{"iv_id"}); my $sth = $dbh->prepare (select_iv() . " where iv_id=?"); $sth->execute ($stuff->{"row"}->{"iv_id"}); my $row = $sth->fetchrow; $stuff->{"row"}->{"iv_duration"} = $row->{"iv_duration"}; } my $client_id = get_id_where ("client", $stuff->{"client_name"}->child->get_text()); my $project_id = get_id_where ("project", $stuff->{"project_name"}->child->get_text(), "client_id" => $client_id); my $sth = $dbh->prepare ('update iv set iv_project_id=?, iv_workdone=? where iv_id=?'); $sth->execute ($project_id, $stuff->{"iv_workdone"}->child->get_text(), $stuff->{iv_id}); $stuff->{"row"} = $dbh->selectrow_hashref (select_iv()." where iv_id=?", undef, $stuff->{"row"}->{"iv_id"}); callback_markdirty ($stuff); TRUE; } sub callback_markdirty { my $stuff = pop; my $dirty = 0; for (qw(client_name project_name iv_workdone hourly_rate iv_starttime)) { my $widget = $stuff->{$_}; $widget = $widget->child if eval { $widget->child }; if (!length $widget->get_text() && !length $stuff->{"row"}->{$_}) { } elsif ($widget->get_text ne $stuff->{"row"}->{$_}) { $dirty = 1; last; } } if (!$dirty && defined $stuff->{"row"}->{"iv_duration"} && ($stuff->{"row"}->{"iv_duration"} != hms2s ($stuff->{"iv_duration"}->get_text()))) { $dirty = 1; } $stuff->{"save"}->set_sensitive ($dirty); return TRUE; } sub delete_event { Gtk2->main_quit; return FALSE; } my $idle_id; my $window = Gtk2::Window->new('toplevel'); $window->set_title("tomtracker"); $window->signal_connect(delete_event => \&delete_event); $window->set_border_width(20); repack(); Gtk2->main; sub repack { my $w; $window->child->destroy while $window->child; my $table = Gtk2::Table->new(11, 3, FALSE); $window->add($table); my $w0; my $w1; $w = new Gtk2::HBox (); $table->attach_defaults ($w, 0, 1, 0, 1); $w->pack_start ($w0 = new Gtk2::Button ("coffee"), FALSE, FALSE, 5); $w0->show; $w->pack_start ($w1 = new Gtk2::Button ("work"), FALSE, FALSE, 5); $w1->show; $w->pack_start ($w2 = new Gtk2::Button ("switch"), FALSE, FALSE, 5); $w2->show; $w->show; my $button = Gtk2::Button->new("Refresh"); $button->signal_connect(clicked => \&repack); $table->attach_defaults($button, 0, 1, 10, 11); $button->show; my (@project_recent, %project, @workdone_recent, %workdone); my $sth = $dbh->prepare ("select * from iv right join project on iv_project_id=project_id order by iv_starttime desc limit 100"); $sth->execute; while (my $row = $sth->fetchrow_hashref) { if ($row->{project_id} && ++$project{$row->{project_id}}->{"saw"} == 1 && @project_recent < 10) { $project{$row->{project_id}}->{"row"} = $row; push @project_recent, $row->{project_id}; } if (++$workdone{$row->{iv_workdone}}->{"saw"} == 1 && @workdone_recent < 10) { $workdone{$row->{iv_workdone}}->{"row"} = $row; push @workdone_recent, $row->{iv_workdone}; } } my (@client_recent, %client); my $sth = $dbh->prepare ("select * from iv left join project on iv_project_id=project_id right join client on client_id=project_client_id order by iv_starttime is null, iv_starttime desc limit 100"); $sth->execute; while (my $row = $sth->fetchrow_hashref) { if (++$client{$row->{client_id}}->{"saw"} == 1 && @client_recent < 10) { $client{$row->{client_id}}->{"row"} = $row; push @client_recent, $row->{client_id}; } } my $w; my $x = 0; $table->attach_defaults ($w = new Gtk2::Label ("Client"),$x++,$x,1,2); $w->show; $table->attach_defaults ($w = new Gtk2::Label ("Project"),$x++,$x,1,2); $w->show; $table->attach_defaults ($w = new Gtk2::Label ("Work done"),$x++,$x,1,2); $w->show; $table->attach_defaults ($w = new Gtk2::Label ("Rate"),$x++,$x,1,2); $w->show; $x++; $table->attach_defaults ($w = new Gtk2::Label (" Start"),$x++,$x,1,2); $w->show; $table->attach_defaults ($w = new Gtk2::Label (" End"),$x++,$x,1,2); $w->show; $table->attach_defaults ($w = new Gtk2::Label (" Duration"),$x++,$x,1,2); $w->show; $table->attach_defaults ($w = new Gtk2::Label (" Amount"),$x++,$x,1,2); $w->show; my $sth = $dbh->prepare (select_iv() . " order by iv_id desc"); $sth->execute; my $row; for (my $y = 2; $y < 10 && ($row = $sth->fetchrow_hashref); $y++) { $x = 0; my $stuff = { "iv_id" => $row->{"iv_id"}, "row" => $row }; if ($y == 2) { Glib::Source->remove ($idle_id) if defined $idle_id; $idle_id = Glib::Timeout->add_seconds (1, sub { my $stuff = pop; if ($stuff->{"row"}->{"iv_starttime"} && !defined $stuff->{"row"}->{"iv_duration"}) { my $s = time - $stuff->{"row"}->{"iv_starttimestamp"}; $stuff->{"iv_duration"}->set_text (s2hms($s)); $stuff->{"iv_endtime"}->set_text (s2datetime(time)); #my $fd; #($fd = $stuff->{"iv_endtime"}->get_default_style->font_desc)->set_weight("bold"); #$stuff->{"iv_endtime"}->modify_font ($fd); $stuff->{"dollars"}->set_text (sprintf ("\$%4.02f", $stuff->{"row"}->{"hourly_rate"} * $s / 3600)); $w1->signal_emit ("clicked"); } else { $w0->signal_emit ("clicked"); } TRUE; }, $stuff); $w1->signal_connect ("clicked" => sub { my $stuff = pop; $w1->modify_bg ("normal", new Gtk2::Gdk::Color (0,65535,0)); $w0->modify_bg ("normal", new Gtk2::Gdk::Color (55555,55555,55555)); $w1->modify_bg ("prelight", new Gtk2::Gdk::Color (0,65535,0)); $w0->modify_bg ("prelight", new Gtk2::Gdk::Color (55555,55555,55555)); return TRUE if !$stuff; if (!$stuff->{"row"} || $stuff->{"row"}->{"iv_duration"}) { $dbh->do ("insert into iv (iv_starttime) values (now())"); &repack; } elsif (!$stuff->{"row"}->{"iv_starttime"}) { $dbh->do ("update iv set iv_starttime=now() where iv_id=?", undef, $stuff->{"row"}->{"iv_id"}); $stuff->{"row"} = $dbh->selectrow_hashref (select_iv()." where iv_id=?", undef, $stuff->{"row"}->{"iv_id"}); $stuff->{"iv_starttime"}->set_text ($stuff->{"row"}->{"iv_starttime"}); } TRUE; }, $stuff); $w0->signal_connect ("clicked" => sub { my $stuff = pop; $w1->modify_bg ("normal", new Gtk2::Gdk::Color (55555,55555,55555)); $w0->modify_bg ("normal", new Gtk2::Gdk::Color (65535,0,0)); $w1->modify_bg ("prelight", new Gtk2::Gdk::Color (55555,55555,55555)); $w0->modify_bg ("prelight", new Gtk2::Gdk::Color (65535,0,0)); if ($stuff && $stuff->{"row"} && $stuff->{"row"}->{"iv_starttime"} && !defined $stuff->{"row"}->{"iv_duration"}) { $dbh->do ("update iv set iv_duration=unix_timestamp(now())-unix_timestamp(iv_starttime) where iv_id=?", undef, $stuff->{"row"}->{"iv_id"}); $stuff->{"row"} = $dbh->selectrow_hashref (select_iv()." where iv_id=?", undef, $stuff->{"row"}->{"iv_id"}); } TRUE; }, $stuff); $w2->signal_connect (clicked => sub { $w0->signal_emit ("clicked"); $dbh->do ("insert into iv (iv_starttime) values (now())"); &repack; TRUE; }); } my $savebutton = Gtk2::Button->new("Save"); $savebutton->signal_connect(clicked => \&callback_save, $stuff); $savebutton->set_state ("insensitive"); $savebutton->show; $stuff->{"save"} = $savebutton; my $w; $w = $stuff->{client_name} = Gtk2::ComboBoxEntry->new_text; for my $client_id (@client_recent) { $w->append_text ($client{$client_id}->{"row"}->{"client_name"}); } $w->child->set_text ($row->{"client_name"}); $w->child->signal_connect("changed" => \&callback_markdirty, $stuff); $w->signal_connect("key-release-event" => \&callback_markdirty, $stuff); $table->attach_defaults($w, $x++, $x, $y, $y+1); $w->show; $w = $stuff->{project_name} = Gtk2::ComboBoxEntry->new_text; for my $project_id (@project_recent) { $w->append_text ($project{$project_id}->{"row"}->{"project_name"}); } $w->child->set_text ($row->{"project_name"}); $w->child->signal_connect("changed" => \&callback_markdirty, $stuff); $w->signal_connect("key-release-event" => \&callback_markdirty, $stuff); $table->attach_defaults($w, $x++, $x, $y, $y+1); $w->show; $w = $stuff->{iv_workdone} = Gtk2::ComboBoxEntry->new_text; for my $workdone (@workdone_recent) { $w->append_text ($workdone); } $w->child->set_text ($row->{"iv_workdone"}); $w->child->signal_connect("changed" => \&callback_markdirty, $stuff); $w->signal_connect("key-release-event" => \&callback_markdirty, $stuff); $table->attach_defaults($w, $x++, $x, $y, $y+1); $w->show; $w = $stuff->{"hourly_rate"} = Gtk2::Entry->new; $w->set ("width-request", 40); $w->set_text (sprintf ("%.2d", $row->{"hourly_rate"})); $w->signal_connect("changed" => \&callback_markdirty, $stuff); $w->signal_connect("key-release-event" => \&callback_markdirty, $stuff); $table->attach_defaults($w, $x++, $x, $y, $y+1); $w->show; $table->attach_defaults($savebutton, $x++, $x, $y, $y+1); $w = $stuff->{"iv_starttime"} = Gtk2::Entry->new; $w->set_text ($row->{"iv_starttime"}); $w->signal_connect("changed" => \&callback_markdirty, $stuff); $w->signal_connect("key-release-event" => \&callback_markdirty, $stuff); $table->attach_defaults($w, $x++, $x, $y, $y+1); $w->show; $w = $stuff->{"iv_endtime"} = Gtk2::Label->new; $w->set_text ($row->{"iv_endtime"}); $table->attach_defaults($w, $x++, $x, $y, $y+1); $w->show; $w = $stuff->{"iv_duration"} = Gtk2::Entry->new; $w->set ("width-request", 75); $w->set_text (s2hms($row->{"iv_duration"})); $w->signal_connect("changed" => \&callback_markdirty, $stuff); $w->signal_connect("key-release-event" => \&callback_markdirty, $stuff); $table->attach_defaults($w, $x++, $x, $y, $y+1); $w->show; $w = $stuff->{"dollars"} = Gtk2::Label->new ($row->{"iv_duration"} ? sprintf ("\$%04.02f", $stuff->{"row"}->{"hourly_rate"} * $row->{"iv_duration"} / 3600) : ""); $w->set ("width-request", 60); $w->show; my $a = Gtk2::Alignment->new (1, 0.5, 0, 0); $a->add ($w); $a->show; $table->attach ($a, $x++, $x, $y, $y+1, ["expand", "fill"], "shrink", 0, 0); } $table->show; $window->show; } sub s2hms { my $s = shift; my $h = sprintf ("%02d:%02d:%02d", $s/3600, int($s/60)%60, $s%60); while ($h =~ s{^(_*:*_*)0}{${1}_}) { } return $h; } sub hms2s { my $hms = shift; my $s = 0; my $sixties = 1; while ($hms =~ s/[:\.](\d{1,2})$// || $hms =~ s/(\d{2})$// || $hms =~ s/^\D*(\d)$//) { my $x = $1; $x =~ s/^0//; $s += $x * $sixties; $sixties *= 60; } return $s; } sub s2datetime { my $s = shift; my @t = localtime ($s); return sprintf ("%04d-%02d-%02d %02d:%02d:%02d", $t[5]+1900, $t[4]+1, $t[3], @t[2,1,0]); } sub get_id_where { my ($table, $name, %where) = @_; my @bind = ($name); my $sth = $dbh->prepare ("select ${table}_id from ${table} where ${table}_name=?" . join ("", map { push @bind, $where{$_}; " and ${table}_${_}=?"; } keys %where)); $sth->execute (@bind); if (my $row = $sth->fetchrow_arrayref) { return $row->[0]; } else { my @ibind = ($name); my $sql = "insert into ${table} (${table}_name" . join ("", map { push @ibind, $where{$_}; ", ${table}_${_}"; } keys %where) . ") values (" . join (", ", map { "?" } @ibind) . ")"; if ($dbh->do ($sql, undef, @ibind)) { return $sth->fetchrow_arrayref->[0] if $sth->execute (@bind); } } return undef; } sub select_iv { "select *, unix_timestamp(iv_starttime) iv_starttimestamp, date_add(iv_starttime,interval iv_duration second) iv_endtime, if(iv_hourly_rate is not null,iv_hourly_rate,if(project_hourly_rate is not null,project_hourly_rate,if(client_hourly_rate is not null,client_hourly_rate,100))) hourly_rate from iv left join project on iv_project_id=project_id left join client on client_id=project_client_id"; } 0;