RT# 78019 - Added total revenue line to Package churn report
[freeside.git] / httemplate / graph / elements / report.html
index 3773fbf..70c3a9e 100644 (file)
@@ -11,9 +11,11 @@ Example:
     #these run parallel to items, and can be given as hashes
     'row_labels'      => \@row_labels,    #required
     'colors'          => \@colors,        #required
+    'bgcolors'        => \@bgcolors,      #optional
     'graph_labels'    => \@graph_labels,  #defaults to row_labels
 
     'links'           => \@links,         #optional
+    'no_graph'        => \@no_graph,      #optional
 
     #these run parallel to the elements of each @item
     'col_labels'      => \@col_labels,    #required
@@ -21,7 +23,7 @@ Example:
 
     #optional
     'nototal'         => 1,
-    'graph_type'      => 'LinesPoints',
+    'graph_type'      => 'LinesPoints',   #can be 'none' for no graph
     'bottom_total'    => 1,
     'sprintf'         => '%u', #sprintf format, overrides default %.2f
     'disable_money'   => 1,
@@ -49,19 +51,19 @@ any delimiter and linked from the elements in @data.
 <% $csv->string %>
 %
 %   my @bottom_total = ();
+%   my $row = 0;
 %   foreach ( @items ) {
 %
 %     my $col = 0;
-%     my $total = 0;
-%     $csv->combine(
-%       shift( @row_labels ),
-%       map { $total += $_; $bottom_total[$col++] += $_; sprintf($sprintf, $_); }
-%         ( @{ shift( @data ) } ),
-%       ( $opt{'nototal'} ? () : sprintf($sprintf, $total) ),
-%     );
-%     unless ( $opt{'nototal'} ) { 
-%       $bottom_total[$col++] += $total; 
-%     } 
+%     my @row = map { sprintf($sprintf, $_) } @{ shift(@data) };
+%     my $total = sum(@row);
+%     push @row, sprintf($sprintf, $total) unless $opt{'nototal'};
+%     unless ($opt{'no_graph'}[$row]) {
+%       foreach (@row) {
+%         $bottom_total[$col++] += $_;
+%       }
+%     }
+%     $csv->combine(shift(@row_labels), @row);
 <% $csv->string %>
 %
 %   }
@@ -77,57 +79,70 @@ any delimiter and linked from the elements in @data.
 %   } 
 %   
 % } elsif ( $cgi->param('_type') =~ /(xls)$/ ) {
-%
-%   #http_header('Content-Type' => 'application/excel' ); #eww
-%   http_header('Content-Type' => 'application/vnd.ms-excel' );
-%   #http_header('Content-Type' => 'application/msexcel' ); #alas
-%   http_header('Content-Disposition' => "attachment;filename=$filename.xls");
+%   #false laziness w/  search/elements/search-xls
+%   my $format = $FS::CurrentUser::CurrentUser->spreadsheet_format;
+%   $filename .= $format->{extension};
+%   
+%   http_header('Content-Type' => $format->{mime_type} );
+%   http_header('Content-Disposition' => qq!attachment;filename="$filename"! );
 %
 %   my $output = '';
 %   my $XLS = new IO::Scalar \$output;
-%   my $workbook = Spreadsheet::WriteExcel->new($XLS)
+%   my $workbook = $format->{class}->new($XLS)
 %     or die "Error opening .xls file: $!";
 %
 %   my $worksheet = $workbook->add_worksheet(substr($opt{'title'},0,31));
 %
-%   my($r,$c) = (0,0);
+%   my($row,$col) = (0,0);
 %
 %   foreach ('', @col_labels, ($opt{'nototal'} ? () : 'Total') ) {
 %     my $header = $_;
-%     $worksheet->write($r, $c++, $header)
+%     $worksheet->write($row, $col++, $header)
 %   }
 %
 %   my @bottom_total = ();
 %   foreach ( @items ) {
-%     $r++;
-%     $c = 0;
+%     $row++;
+%     $col = 0;
 %     my $total = 0;
-%     $worksheet->write( $r, $c++, shift( @row_labels ) );
+%     $worksheet->write( $row, $col++, shift( @row_labels ) );
 %     foreach ( @{ shift( @data ) } ) {
 %       $total += $_;
-%       $bottom_total[$c-1] += $_;
-%       $worksheet->write($r, $c++,  sprintf($sprintf, $_) );
+%       $bottom_total[$col-1] += $_ unless $opt{no_graph}[$row];
+%       $worksheet->write_number($row, $col++,  sprintf($sprintf, $_) );
 %     }
-%     unless ( $opt{'nototal'} ) { 
-%       $bottom_total[$c-1] += $total
-%       $worksheet->write($r, $c++,  sprintf($sprintf, $total) );
+%     if ( !$opt{'nototal'} ) {
+%       $bottom_total[$col-1] += $total unless $opt{no_graph}[$row]
+%       $worksheet->write_number($row, $col++,  sprintf($sprintf, $total) );
 %     } 
 %   }
 % 
-%   $c = 0;
+%   $col = 0;
 %   if ( $opt{'bottom_total'} ) {
-%     $r++;
-%     $worksheet->write($r, $c++, 'Total');
-%     $worksheet->write($r, $c++, sprintf($sprintf, $_)) foreach @bottom_total;
+%     $row++;
+%     $worksheet->write($row, $col++, 'Total');
+%     $worksheet->write_number($row, $col++, sprintf($sprintf, $_)) foreach @bottom_total;
 %   } 
 %   
 %   $workbook->close();# or die "Error creating .xls file: $!";
 %
 %   http_header('Content-Length' => length($output) );
-%   
-<% $output %>
-% } elsif ( $cgi->param('_type') eq 'png' ) {
+%   $m->print($output);
 %
+% } elsif ( $cgi->param('_type') eq 'png' ) {
+%   # delete any items that shouldn't be on the graph
+%   if ( my $no_graph = $opt{'no_graph'} ) {
+%     my $i = 0;
+%     while (@$no_graph) {
+%       if ( shift @$no_graph ) {
+%         splice @data, $i, 1;
+%         splice @{$opt{'graph_labels'}}, $i, 1;
+%         splice @{$opt{'colors'}}, $i, 1;
+%         $i--; # because everything is shifted down
+%       }
+%       $i++;
+%     }
+%   }
 %   my $graph_type = 'LinesPoints';
 %   if ( $opt{'graph_type'} =~ /^(LinesPoints|Mountain|Bars)$/ ) {
 %     $graph_type = $1;
@@ -135,7 +150,21 @@ any delimiter and linked from the elements in @data.
 %   my $class = "Chart::$graph_type";
 %
 %   my $chart = $class->new(976,384);
-%   
+% # the chart area itself is 900 pixels wide, and the date labels are ~60 each.
+% # staggered, we can fit about 28 of them.
+% # they're about 12 pixels high, so vertically, we can fit about 60 (allowing
+% # space for them to be readable).
+% # after that we have to start skipping labels. also remove the dots, since 
+% # they're just a blob at that point.
+%   my $num_labels = scalar(@{ $opt{axis_labels} });
+%   my %chart_opt = %{ $opt{chart_options} || {} };
+%   if ( $num_labels > 28 ) {
+%     $chart_opt{x_ticks} = 'vertical';
+%     if ( $num_labels > 60 ) {
+%       $chart_opt{skip_x_ticks} = int($num_labels / 60) + 1;
+%       $chart_opt{pt_size} = 1;
+%     }
+%   }
 %   my $d = 0;
 %   $chart->set(
 %     #'min_val' => 0,
@@ -152,6 +181,7 @@ any delimiter and linked from the elements in @data.
 %                 },
 %     'legend_labels' => $opt{'graph_labels'},
 %     'brush_size' => 4,
+%     %chart_opt,
 %   );
 %
 %   http_header('Content-Type' => 'image/png' );
@@ -169,105 +199,130 @@ any delimiter and linked from the elements in @data.
 <% include('/elements/header.html', $opt{'title'} ) %>
 % unless ( $opt{'graph_type'} eq 'none' ) {
 
-<IMG SRC="<% "$myself;_type=png" %>" WIDTH="976" HEIGHT="384">
+<IMG SRC="<% "$myself;_type=png" %>" WIDTH="976" HEIGHT="384"
+ STYLE="page-break-after:always;">
 % }
-<P ALIGN="right">
+<P ALIGN="right" CLASS="noprint">
 
 % unless ( $opt{'disable_download'} ) { 
             Download full results<BR>
             as <A HREF="<% "$myself;_type=xls" %>">Excel spreadsheet</A><BR>
             as <A HREF="<% "$myself;_type=csv" %>">CSV file</A></P>
-% } 
+% }
 %
 </P>
-<% include('/elements/table.html', 'f8f8f8') %>
-
-<TR>
-
-  <TD></TD>
-
+%# indexed by item, then by entry (the element indices of @{$data[$i]}).
+% my @cell = ();
+% my @styles;
+% my $num_entries = scalar(@col_labels);
+% my $num_items = scalar(@items);
+% $cell[0] = ['']; #top left corner
 % foreach my $column ( @col_labels ) {
-%       $column =~ s/ /\<BR\>/; # working on a smarter way to do this
-    <TH><% $column %></TH>
-% } 
-
-% unless ( $opt{'nototal'} ) { 
-    <TH>Total</TH>
-% } 
-
-</TR>
+%   $column =~ s/ /\<BR\>/;
+%   push @{$cell[0]}, $column;
+% }
+% if ( ! $opt{'nototal'} ) {
+%   $num_entries++;
+%   push @{$cell[0]}, emt('Total');
+% }
 
-% my @bottom_total = ();
+% # i for item, e for entry
+% my $i = 1;
+% my @bottom_total = map {0} @col_labels;
 % foreach my $row ( @items ) {
+% #make a style
+%   my $color = shift @{ $opt{'colors'} };
+%   my $bgcolor = $opt{'bgcolors'} ? (shift @{ $opt{'bgcolors'} }) : 'ffffff';
+%   push @styles, ".i$i { text-align: right; color: #$color; background: #$bgcolor; }";
+% #create the data row
+%   my $links = shift @{$opt{'links'}} || [''];
+%   my $link_prefix = shift @$links;
+%   $link_prefix = '<A CLASS="cell" HREF="'.$link_prefix if $link_prefix;
+%   my $label = shift @row_labels;
+%   $cell[$i] = [ $label ];
 %
-%     my $color = shift( @{ $opt{'colors'} } );
-%     my @links = @{ shift( @{ $opt{'links'} } ) };
-% #   $opt{'links'} is an array parallel to items.
-% #   Each element of that is an array containing a prefix,
-% #   followed by suffixes matched to the cells of the table.
-%     my $link_prefix = shift @links;
-%     $link_prefix = $link_prefix ? qq(<A HREF="$link_prefix) : '';    #"
-%     my $label = shift( @row_labels );
-
-      <TR>
-
-        <TH>
-          <FONT COLOR="#<% $color %>"><% $label %></FONT>
-        </TH>
+%   my $data_row = $data[$i-1];
+%#   my $data_row = shift @data;
+%   if ( ! $opt{'nototal'} ) {
+%     push @$data_row, sum(@$data_row);
+%   }
+%   my $e = 0;
+%   foreach ( @$data_row ) {
+%     my $entry = $_;
+%     $entry = $money_char . sprintf($sprintf_fields->{$row} ? $sprintf_fields->{$row} : $sprintf, $entry);
+%     $entry = $link_prefix . shift(@$links) . "\">$entry</A>" if $link_prefix;
+%     push @{$cell[$i]}, $entry;
+%     $bottom_total[$e++] += $_ unless $opt{no_graph}[$i-1];
+%   }
+%   $i++;
+% }
+% if ( $opt{'bottom_total'} ) {
+%   # it's an extra item
+%   $num_items++;
+%   push @styles, ".i$i { text-align: right; background-color: #f5f6be; }";
+%   my $links = $opt{'bottom_link'} || [];
+%   my $link_prefix = shift @$links;
+%   $link_prefix = '<A CLASS="cell" HREF="'.$link_prefix if $link_prefix;
+%   $cell[$i] = [ emt('Total') ];
+%   for (my $e = 0; $e < $num_entries + 1; $e++) {
+%     my $entry = $bottom_total[$e];
+%     $entry = $money_char . sprintf($sprintf, $entry);
+%     $entry = $link_prefix . shift(@$links) . "\">$entry</A>" if $link_prefix;
+%     push @{$cell[$i]}, $entry;
+%   }
+% }
 
-%       my $total = 0;
-%       my $col = 0;
-%       foreach my $column ( @{ shift( @data ) } ) {
+<STYLE type="text/css">
+a.cell {
+  color: inherit !important;
+}
+td.cell {
+  border-color: #000;
+}
+<% join("\n", @styles) %>
+%# item labels
+.e0 {
+  text-align: center;
+  font-weight: bold;
+}
+%# totals
+% if ( ! $opt{'nototal'} ) {
+.e<% $num_entries %> {
+  text-align: right;
+  background-color: #f5f6be;
+}
+% }
+%# date labels
+.i0 {
+  text-align: center;
+  font-weight: bold;
+}
+</STYLE>
 
-          <TD ALIGN="right" BGCOLOR="#ffffff">
-            <% $link_prefix ? $link_prefix . shift(@links) . '">' : '' %><FONT COLOR="#<% $color %>"><% $money_char %><% sprintf($sprintf,, $column) %></FONT><% $link_prefix ? '</A>' : '' %>
-          </TD>
+<% include('/elements/table.html', 'f8f8f8') %>
+% if ( $opt{'transpose'} ) {
+%   for ( my $e = 0; $e < $num_entries + 1; $e++ ) {
+  <TR>
+%     for ( my $i = 0; $i < $num_items + 1; $i++ ) {
+    <TD CLASS="<%"cell i$i e$e"%>"><% $cell[$i][$e] %></TD>
+%     }
+  </TR>
+%   }
 %
-%         $total += $column;
-%         $bottom_total[$col++] += $column;
-%      
-%       } 
-
-%       unless ( $opt{'nototal'} ) { 
-            <TD ALIGN="right" BGCOLOR="#f5f6be">
-              <% $link_prefix ? $link_prefix. shift(@links) . '">' : '' %><FONT COLOR="#<% $color %>"><% $money_char %><% sprintf($sprintf, $total) %></FONT><% $link_prefix ? '</A>' : '' %>
-            </TD>
-%           $bottom_total[$col++] += $total; 
-%       } 
-
-      </TR>
-
-% } 
-
-% if ( $opt{'bottom_total'} ) {
+% } else { #!transpose
+%
+%   for (my $i = 0; $i < $num_items + 1; $i++) {
   <TR>
-    <TH>Total</TH>
-%   my @bottom_links = $opt{'bottom_link'} ? @{ $opt{'bottom_link'} } : ();
-%   my $prefix = shift(@bottom_links);
-%   pop @bottom_links if $opt{'nototal'};
-%   foreach my $total ( @bottom_total ) { 
-
-      <TD ALIGN="right" BGCOLOR="#f5f6be">
-        <% $prefix
-              ? '<A HREF="'. $prefix .shift(@bottom_links). '">'
-              : ''
-        %><% $money_char %><% sprintf($sprintf, $total) %><% $prefix ? '</A>' : '' %>
-
-      </TD>
-
-% } 
-
+%     for (my $e = 0; $e < $num_entries + 1; $e++) {
+    <TD CLASS="<%"cell i$i e$e"%>"><% $cell[$i][$e] %></TD>
+%     }
   </TR>
-
-% } 
-
+%   }
 </TABLE>
+% }
 
 <% include('/elements/footer.html') %>
 % } 
-<%once>
-
-</%once>
 <%init>
 
 my(%opt) = @_;
@@ -278,7 +333,7 @@ if ( $cgi->param('session') =~ /^(\d+)$/ ) {
   %opt = %{ $m->cache->get($session) };
 }
 else {
-  $session = sprintf("%10d%6d", time, int(rand(1000000)));
+  $session = sprintf("%010d", random_id(10));
   $m->cache->set($session, \%opt, '1h');
 }
 
@@ -288,6 +343,7 @@ my $conf = new FS::Conf;
 my $money_char = $opt{'disable_money'} ? '' : $conf->config('money_char');
 
 my @items = @{ $opt{'items'} };
+my $sprintf_fields = $opt{'sprintf_fields'};
 
 foreach my $other (qw( col_labels row_labels graph_labels axis_labels colors links )) {
   if ( ref($opt{$other}) eq 'HASH' ) {