%# TimeWorkedReport - Version 1.4 %# https://github.com/coffeemonster/rt-extension-timeworkedreport#readme <%args> $startdate => undef $enddate => undef $byticket => undef $queues => undef $user => undef <& /Elements/Header, Title => $title &> <& /Elements/Tabs &>
<%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' ); }

<&|/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)
<&|/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)
<& /Elements/SelectQueue, Multiple => 1, Name => 'queues', Id => 'queues', Default => ($queues||'') &>
% my $userobject = RT::Queue->new($session{CurrentUser}); <& /Elements/SelectOwnerDropdown, Name=>'user', Objects => [$userobject], Default => ($user||'') &>
<& /Elements/Checkbox, Name => 'byticket', Default => ($byticket) ? 'checked' : ''&> Organize report by ticket instead of by person <& /Elements/Submit&>
<%perl> 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, %tstats); # Get a new transactions object to hold transaction search results for this ticket my $trans = RT::Transactions->new( $session{'CurrentUser'} ); # only in the period of interest $trans->Limit( FIELD => 'Created', OPERATOR => '>', VALUE => $startdate ); $trans->Limit( FIELD => 'Created', OPERATOR => '<', VALUE => $enddate, ENTRYAGGREGATOR => 'AND'); # only user of interest $trans->Limit( FIELD => 'Creator', OPERATOR => '=', VALUE => $user, ENTRYAGGREGATOR => 'AND') if $user; # 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; } # Ticket Stats if ( !exists( $tstats{ $t->id }{Subject} ) ) { $tstats{ $t->id }{Subject} = $t->Subject; if ($t->Members) { my @children; while (my $link = $t->Members->Next) { push @children, $link->BaseObj->Id if $link->BaseObj; } $tstats{ $t->id }{Children} = \@children; } if ($t->MemberOf) { my @parents; while (my $link = $t->MemberOf->Next) { # don't self-include push @parents, $link->TargetObj->Id if ($link->TargetObj && $link->TargetObj->Id != $t->id ); } $tstats{ $t->id }{Parents} = \@parents; } } 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; # ticket-centric $tstats{ $t->id }{People}{$tr->CreatorObj->Name} += $tr->TimeTaken; $tstats{ $t->id }{TotalTime} += $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; $tstats{ $t->id }{People}{$tr->CreatorObj->Name} += $tr->NewValue - $tr->OldValue; $tstats{ $t->id }{TotalTime} += $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 "

TIME WORKED REPORT FOR QUEUE(S) " . $queuelist . "

"; print "

Date Range: $startdate TO $enddate

"; if ($byticket) { print "

Organized by Ticket

"; } print "
"; # 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 => 'SeeGroup', Object => $RT::System ) ) { my %tempstats; $tempstats{ $session{CurrentUser}->Name } = $stats{ $session{CurrentUser}->Name }; %stats = %tempstats; } # Report - grouped by ticket (with children nested if applicable) if ($byticket) { # calculate GrandTotal for parent tickets. foreach my $tid (keys %tstats) { if (my @parents = @{$tstats{$tid}{Parents}} ) { foreach my $p_id (@parents) { next unless $tstats{$p_id}; $tstats{$p_id}{GrandTotal} += $tstats{$tid}{TotalTime}; } } } my %seen; # Now emit the report sorted by GrandTotal, TotalTime } for my $tid ( sort { $tstats{$b}{GrandTotal} <=> $tstats{$a}{GrandTotal} || $tstats{$b}{TotalTime} <=> $tstats{$a}{TotalTime} } keys %tstats ) { # only show a ticket once $seen{$tid}++ and next; % my $ticket = $tstats{$tid}; % my $subject = $ticket->{Subject};

"><% $tid %>: <% $ticket->{Subject}%>

% if ($ticket->{GrandTotal}) { % my $grand_total = $ticket->{GrandTotal} + $ticket->{TotalTime}; % } % for my $person ( sort { $ticket->{People}{$b} <=> $ticket->{People}{$a} } keys %{ $ticket->{People} }) { % my $minutes = $ticket->{People}{$person}; % } % if ($ticket->{Children}) { % foreach my $cid (@{ $ticket->{Children} }) { % my $child = $tstats{$cid}; % } % }
<% $grand_total %>m<% sprintf('%.1fh', $grand_total / 60 ) %>GRAND TOTAL
<% $ticket->{TotalTime} %>m<% sprintf('%.1fh', $ticket->{TotalTime} / 60 ) %>TOTAL TIME
<% $minutes %>m<% sprintf('%.1fh', $minutes/60) %><% $person %>

"><% $cid %>: <% $child->{Subject}%>

% $seen{$cid}++ and next; % for my $person ( sort { $child->{People}{$b} <=> $child->{People}{$a} } keys %{ $child->{People} }) { % my $minutes = $child->{People}{$person}; % }
<% $child->{TotalTime} %>m<% sprintf('%.1fh', $child->{TotalTime} / 60 ) %>TOTAL TIME
<% $minutes %>m<% sprintf('%.1fh', $minutes/60) %><% $person %>
<%perl> } } # Report - grouped by person 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 "

" . ($personobj->FriendlyName || $personobj->Name) . "

"; print ""; print ""; 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 = "$tid: " . $stats{$person}{$tid}{Subject}; my $href = RT->Config->Get('WebPath'). "/Ticket/Display.html?id=$tid"; print "" . ""; $totalMinutes += $minutes; } print ""; print "
MINUTESHOURSTICKET
${minutes}m" . sprintf( "%.1fh", ( $minutes / 60 ) ) . "$subject
${totalMinutes}m" . sprintf( "%.1fh", ( $totalMinutes / 60 ) ) . "TOTALS
"; } } ##### 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 ); } # use Data::Dumper; print "
". Dumper \%tstats; print Dumper \%stats;
    # vim: syn=mason