The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
<%args>
     $startdate => undef
     $enddate   => undef
     $queues    => undef
     $byticket  => undef
</%args>

<& /Elements/Header, Title => $title &>
<& /Elements/Tabs &>
<hr>

<%init>
my ($start_date, $end_date, $effective_end_date, $title);

$title = loc('Time worked report');

$start_date = RT::Date->new($session{'CurrentUser'});
$end_date   = RT::Date->new($session{'CurrentUser'});

# If we have a value for start date, parse it into an RT::Date object
if ($startdate) {
  $start_date->Set(Format => 'unknown', Value => $startdate);
  # And then get it back as an ISO string for display purposes, in the form field and
  # report header
  $startdate = $start_date->AsString(Format => 'ISO', Timezone => 'server');
}

# Same treatment for end date
if ($enddate) {
  $end_date->Set(Format => 'unknown', Value => $enddate);
  $enddate = $end_date->AsString(Format => 'ISO', Timezone => 'server');
}

</%init>

<form method="post" action="TimeWorkedReport.html">
 <br />
  <&|/l&>Start date</&>:
  <& /Elements/SelectDate, Name => 'startdate', Default => ($startdate) ?  $start_date->AsString(Format => 'ISO', Timezone => 'server') : ''&>
  (report will start from midnight on this day unless you indicate otherwise)
 <br />
  <&|/l&>End date</&>:
  <& /Elements/SelectDate, Name => 'enddate', Default => ($enddate) ?  $end_date->AsString(Format => 'ISO', Timezone => 'server') : ''&>
  (report will -not- be inclusive of this day unless you change the time from midnight)
 <br />
  <&|/l&>Queues</&>:
  <& /Elements/SelectQueue, Multiple => 1, Name => 'queues', Default => ($queues) ? $queues : '' &>

 <br />
  <& /Elements/Checkbox, Name => 'byticket', Default => ($byticket) ? 'checked' : ''&>
  Organize report by ticket instead of by person
<& /Elements/Submit&>

</form>


<%perl>
# TimeWorkedReport
# Version 0.04  2009-09-28
#
# Fran Fabrizio, UAB CIS, fran@cis.uab.edu

use strict;

# if we are just getting here and the form values are empty, we are done
if (!$startdate || !$enddate) {
  return;
}

# get the queue object(s)
my $queuesobj = new RT::Queues($session{CurrentUser});
my ($queuelist, %queuesofinterest);

# The user's choice of queues will come in from the web form in the $queues variable, which is
# mapped to the SELECT field on the web interface for the report.  Unfortunately, if the user
# chooses just one queue, $queues will have a scalar value, but if the user chooses multiple
# queues, it will be an arrayref.  So we need to check for each case and process differently.
#
# What we want to construct is the %queuesofinterest simple lookup hash which defines a key
# that is the queue ID for each queue selected, and the $queuelist string, which is just for
# displaying the list of queues in the report header
$queues = [ $queues ] unless ref($queues);

for (@$queues) {
  $queuesobj->Limit(FIELD => "Id", OPERATOR => "=", VALUE => $_, ENTRYAGGREGATOR => "OR");
  $queuesofinterest{$_} = 1;
}
$queuelist = join ", ", map {$_->Name} @{$queuesobj->ItemsArrayRef};

# hash to hold statistics
# %stats will be a multilevel hash - first level keys are the usernames, second level keys are
# the ticket IDs, and for each ticket, we store an anonymous hash with keys Subject and  TimeWorked
# (this implies that a single ticket can live under two+ users if they both worked the ticket)
my %stats;

# Get a new transactions object to hold transaction search results for this ticket
my $trans = new RT::Transactions($session{'CurrentUser'});

# only in the period of interest
$trans->Limit(FIELD => 'Created', OPERATOR => '>', VALUE => $startdate);
$trans->Limit(FIELD => 'Created', OPERATOR => '<', VALUE => $enddate, ENTRYAGGREGATOR =>  'AND');

# now start counting all the TimeTaken by examining transactions associated with this ticket
while (my $tr = $trans->Next) {

 # did this transaction take any time?  RT records this -either- in TimeTaken column or by
 # indicating "TimeWorked" in the Field column, depending on how the user inputted the time.
 if (($tr->TimeTaken != 0) || ($tr->Field && $tr->Field eq 'TimeWorked')) {
   # Got a hot one - what ticket is this?
   my $t = new RT::Ticket($session{'CurrentUser'});
   $t->Load($tr->ObjectId);

   if (!$t) {
     # unable to retrieve a ticket for this transaction
     # hopefully we don't ever reach here!
     next;
   } else {
     # Is a queue selected and is this ticket in a queue we care about?
     if ($queuelist && !$queuesofinterest{$t->Queue}) {
       next;
     }
   }

   # If this is time logged by user RT_System, it's the result of a ticket merge
   # In order to avoid double-counting minutes in --byticket mode, or the less serious
   # issue of displaying a report for user RT_System in normal mode, we skip this entirely
   if ($tr->CreatorObj->Name eq 'RT_System') {
     next;
   }

   # we've got some time to account for

   # is this the first time this person is charging time to this ticket?
   # if so, add this ticket subject to the data structure
   if (!exists($stats{$tr->CreatorObj->Name}{$t->id}{Subject})) {
     $stats{$tr->CreatorObj->Name}{$t->id}{Subject} = $t->Subject;
   }

   if ($tr->TimeTaken != 0) {
     # this was a comment or correspondence where the user also added some time worked
     # value of interest appears in Transaction's TimeTaken column
     $stats{$tr->CreatorObj->Name}{$t->id}{TimeWorked} += $tr->TimeTaken;
   } else {
     # this was a direct update of the time worked field from the Basics or Jumbo ticket update page
     # values of interest appear in Transaction's OldValue and NewValue columns
     # RT does not use the TimeTaken column in this instance.
     $stats{$tr->CreatorObj->Name}{$t->id}{TimeWorked} += $tr->NewValue - $tr->OldValue;
   }
 }
}

# report output starts here
# output:
#  normal user: their own time worked report, most worked ticket to least worked ticket
#  superuser:   everyone's time worked report, in username alpha order, then by most worked to least worked
#  superuser+byticket: most worked ticket first, with everyone's contribution ranked by  biggest contribution to smallest

print "<h2>TIME WORKED REPORT FOR QUEUE(S) " . $queuelist . "</h2>";
print "<h3>Date Range: $startdate TO $enddate</h3>";
if ($byticket) {
  print "<h3>Organized by Ticket</h3>";
}
print "<hr>";

# if this person is not a superuser, we should only show them the report for themselves
# which means we should remove all keys from %stats except their own username
if (!($session{'CurrentUser'}->HasRight(Right => 'SuperUser', Object => $RT::System))) {
  my %tempstats;
  $tempstats{$session{CurrentUser}->Name} = $stats{$session{CurrentUser}->Name};
  %stats = %tempstats;
}

if ($byticket) {
  # if we're going to organize this by ticket, we need to transform the data first
  # HAVE ENTRIES LIKE:  $stats{JoeUser}{12345}{TimeWorked} = 150
  #                     $stats{JoeUser}{12345}{Subject} = "Fix the Fubar Widget"
  # WANT ENTRIES LIKE:  $tstats{12345}{TotalTime} = 250
  #                     $tstats{12345}{Subject} = "Fix the Fubar Widget"
  #                     $tstats{12345}{People}{JoeUser} = 150
  #                     $tstats{12345}{People}{JaneDoe} = 100

  my %tstats;
  for my $person (keys %stats) {
    for my $tid (keys %{$stats{$person}}) {
      # grab the subject line if you don't have it already
      if (!exists($tstats{$tid}{Subject})) {
        $tstats{$tid}{Subject} = $stats{$person}{$tid}{Subject};
      }
      # now increment total time for this ticket
      $tstats{$tid}{TotalTime} += $stats{$person}{$tid}{TimeWorked};
      # and record this user's contribution to this ticket
      $tstats{$tid}{People}{$person} = $stats{$person}{$tid}{TimeWorked};
    }
  }

  # Now emit the report
  for my $tid (sort {$tstats{$b}{TotalTime} <=> $tstats{$a}{TotalTime}} keys %tstats) {
    my $subject = $tstats{$tid}{Subject};
    print "<H3><A TARGET=\"_TimeWorked\" HREF=\"/Ticket/Display.html?id=$tid\">$tid:  $subject</A></H3>";
    print "<TABLE BORDER=0 CELLSPACING=5>";
    printf("<TR><TH WIDTH=30></TH><TH>%dm</TH><TH>%.1fh</TH><TH>TOTAL TIME</TH></TR>",  $tstats{$tid}{TotalTime},($tstats{$tid}{TotalTime} / 60));
    for my $person (sort {$tstats{$tid}{People}{$b} <=> $tstats{$tid}{People}{$a}} keys %{$tstats{$tid}{People}}) {
      my $minutes = $tstats{$tid}{People}{$person};
      printf("<TR><TD></TD><TD>%dm</TD><TD>%.1fh</TD><TD>%s</TD></TR>",$minutes,($minutes /60),$person);
    }
    print "</TABLE>";
  }
} else {
  # the existing %stats data structure is perfect for the default report, no data transform  needed
  for my $person (sort keys %stats) {
    # get the person object, so we can get the FriendlyName to use as header
    my $personobj = new RT::User($session{CurrentUser});
    $personobj->Load($person);

    print "<h3>" . $personobj->FriendlyName . "</h3>";
    print "<TABLE BORDER=0 CELLSPACING=5>";
    print "<TR><TH>MINUTES</TH><TH>HOURS</TH><TH>TICKET</TH></TR>";
    my $totalMinutes = 0;
    for my $tid (sort {$stats{$person}{$b}{TimeWorked} <=> $stats{$person}{$a}{TimeWorked}}  keys %{$stats{$person}}) {
      my $minutes = $stats{$person}{$tid}{TimeWorked};
      my $subject = $stats{$person}{$tid}{Subject};
      print "<TR><TD ALIGN=RIGHT>${minutes}m</TD><TD ALIGN=RIGHT>" . sprintf("%.1fh",($minutes/60)) . "</TD>" .
                "<TD><A TARGET=\"_TimeWorked\" HREF=\"/Ticket/Display.html?id=$tid\">$tid:  $subject</A></TD></TR>";
      $totalMinutes += $minutes;
    }
    print "<TR><TD ALIGN=RIGHT><B>${totalMinutes}m</B></TD><TD ALIGN=RIGHT><B>" . sprintf("%.1fh",($totalMinutes/60)) . "</B></TD><TD><B>TOTALS</B></TD></TR>";
    print "</TABLE>";
  }
}

##### helper functions below

sub form_date_string {
 # expects seven input params - year, month, day, hour, minute, second, offset
 my $year = $_[0] - 1900;
 my $mon = $_[1] - 1;
 my $day = $_[2];
 my $hour = $_[3] ? $_[3] : 0;
 my $min = $_[4] ? $_[4] : 0;
 my $sec = $_[5] ? $_[5] : 0;
 my $offset = $_[6] ? $_[6] : 0;

 # convert to seconds since epoch, then adjust for the $offset, which is also in seconds
 # we do this so we don't have to do fancy date arithmetic - we can just subtract one seconds
 # value from the other seconds value
 my $starttime = timelocal($sec,$min,$hour,$day,$mon,$year) - $offset;

 # convert back to component parts now that we've adjusted for offset
 # this gives us the components which represent the GMT time for the local time that was entered
 # on the command line
 ($sec,$min,$hour,$day,$mon,$year) = localtime($starttime);

 # format the date string, padding with zeros if needed
 return sprintf("%04d-%02d-%02d %02d:%02d:%02d", ($year+1900), ($mon+1), $day, $hour, $min, $sec);
}

</%perl>