customer signup report, #17050
[freeside.git] / httemplate / graph / elements / report.html
1 <%doc>
2
3 Example:
4
5   include('elements/report.html',
6     #required
7     'title'           => 'Page title',
8     'items'           => \@items,
9     'data'            => [ \@item1 \@item2 ... ],
10
11     #these run parallel to items, and can be given as hashes
12     'row_labels'      => \@row_labels,    #required
13     'colors'          => \@colors,        #required
14     'graph_labels'    => \@graph_labels,  #defaults to row_labels
15
16     'links'           => \@links,         #optional
17
18     #these run parallel to the elements of each @item
19     'col_labels'      => \@col_labels,    #required
20     'axis_labels'     => \@axis_labels,   #defaults to col_labels
21
22     #optional
23     'nototal'         => 1,
24     'graph_type'      => 'LinesPoints',
25     'bottom_total'    => 1,
26     'sprintf'         => '%u', #sprintf format, overrides default %.2f
27     'disable_money'   => 1,
28   );
29
30 About @links: Each element must be an arrayref, corresponding to an element of
31 @items.  Within the array, the first element is a URL prefix, and the rest 
32 are suffixes corresponding to data elements.  These will be joined without 
33 any delimiter and linked from the elements in @data.
34
35 </%doc>
36 % if ( $cgi->param('_type') =~ /^(csv)$/ ) {
37 %
38 %   #http_header('Content-Type' => 'text/comma-separated-values' ); #IE chokes
39 %   #http_header('Content-Type' => 'text/plain' );
40 %   http_header('Content-Type' => 'text/csv');
41 %   http_header('Content-Disposition' => "attachment;filename=$filename.csv");
42 %
43 %   my $csv = new Text::CSV_XS { 'always_quote' => 1,
44 %                                'eol'          => "\n", #"\015\012", #"\012"
45 %                              };
46 %
47 %   $csv->combine('', @col_labels, $opt{'nototal'} ? () : 'Total');
48 %   
49 <% $csv->string %>
50 %
51 %   my @bottom_total = ();
52 %   foreach ( @items ) {
53 %
54 %     my $col = 0;
55 %     my $total = 0;
56 %     $csv->combine(
57 %       shift( @row_labels ),
58 %       map { $total += $_; $bottom_total[$col++] += $_; sprintf($sprintf, $_); }
59 %         ( @{ shift( @data ) } ),
60 %       ( $opt{'nototal'} ? () : sprintf($sprintf, $total) ),
61 %     );
62 %     unless ( $opt{'nototal'} ) { 
63 %       $bottom_total[$col++] += $total; 
64 %     } 
65 <% $csv->string %>
66 %
67 %   }
68
69 %   if ( $opt{'bottom_total'} ) {
70 %     $csv->combine(
71 %       'Total',
72 %       map { sprintf($sprintf, $_) } @bottom_total,
73 %     );
74 %
75 <% $csv->string %>
76 %
77 %   } 
78 %   
79 % } elsif ( $cgi->param('_type') =~ /(xls)$/ ) {
80 %
81 %   #http_header('Content-Type' => 'application/excel' ); #eww
82 %   http_header('Content-Type' => 'application/vnd.ms-excel' );
83 %   #http_header('Content-Type' => 'application/msexcel' ); #alas
84 %   http_header('Content-Disposition' => "attachment;filename=$filename.xls");
85 %
86 %   my $output = '';
87 %   my $XLS = new IO::Scalar \$output;
88 %   my $workbook = Spreadsheet::WriteExcel->new($XLS)
89 %     or die "Error opening .xls file: $!";
90 %
91 %   my $worksheet = $workbook->add_worksheet(substr($opt{'title'},0,31));
92 %
93 %   my($r,$c) = (0,0);
94 %
95 %   foreach ('', @col_labels, ($opt{'nototal'} ? () : 'Total') ) {
96 %     my $header = $_;
97 %     $worksheet->write($r, $c++, $header)
98 %   }
99 %
100 %   my @bottom_total = ();
101 %   foreach ( @items ) {
102 %     $r++;
103 %     $c = 0;
104 %     my $total = 0;
105 %     $worksheet->write( $r, $c++, shift( @row_labels ) );
106 %     foreach ( @{ shift( @data ) } ) {
107 %       $total += $_;
108 %       $bottom_total[$c-1] += $_;
109 %       $worksheet->write($r, $c++,  sprintf($sprintf, $_) );
110 %     }
111 %     unless ( $opt{'nototal'} ) { 
112 %       $bottom_total[$c-1] += $total; 
113 %       $worksheet->write($r, $c++,  sprintf($sprintf, $total) );
114 %     } 
115 %   }
116
117 %   $c = 0;
118 %   if ( $opt{'bottom_total'} ) {
119 %     $r++;
120 %     $worksheet->write($r, $c++, 'Total');
121 %     $worksheet->write($r, $c++, sprintf($sprintf, $_)) foreach @bottom_total;
122 %   } 
123 %   
124 %   $workbook->close();# or die "Error creating .xls file: $!";
125 %
126 %   http_header('Content-Length' => length($output) );
127 %   
128 <% $output %>
129 % } elsif ( $cgi->param('_type') eq 'png' ) {
130 %
131 %   my $graph_type = 'LinesPoints';
132 %   if ( $opt{'graph_type'} =~ /^(LinesPoints|Mountain|Bars)$/ ) {
133 %     $graph_type = $1;
134 %   }
135 %   my $class = "Chart::$graph_type";
136 %
137 %   my $chart = $class->new(976,384);
138 % # the chart area itself is 900 pixels wide, and the date labels are ~60 each.
139 % # staggered, we can fit about 28 of them.
140 % # they're about 12 pixels high, so vertically, we can fit about 60 (allowing
141 % # space for them to be readable).
142 % # after that we have to start skipping labels. also remove the dots, since 
143 % # they're just a blob at that point.
144 %   my $num_labels = scalar(@{ $opt{axis_labels} });
145 %   my %chart_opt = %{ $opt{chart_options} || {} };
146 %   if ( $num_labels > 28 ) {
147 %     $chart_opt{x_ticks} = 'vertical';
148 %     if ( $num_labels > 60 ) {
149 %       $chart_opt{skip_x_ticks} = int($num_labels / 60) + 1;
150 %       $chart_opt{pt_size} = 1;
151 %     }
152 %   }
153 %   my $d = 0;
154 %   $chart->set(
155 %     #'min_val' => 0,
156 %     'legend' => 'bottom',
157 %     'colors' => { ( 
158 %                     map { my $color = $_;
159 %                           'dataset'.$d++ =>
160 %                             [ map hex($_), unpack 'a2a2a2', $color ]
161 %                         }
162 %                         @{ $opt{'colors'} }
163 %                   ),
164 %                   'grey_background' => 'white',
165 %                   'background' => [ 0xe8, 0xe8, 0xe8 ], #grey
166 %                 },
167 %     'legend_labels' => $opt{'graph_labels'},
168 %     'brush_size' => 4,
169 %     %chart_opt,
170 %   );
171 %
172 %   http_header('Content-Type' => 'image/png' );
173 %   http_header('Cache-Control' => 'no-cache' );
174 %
175 %   $chart->_set_colors();
176 %   
177 <% $chart->scalar_png([ $opt{'axis_labels'}, @data ]) %>
178 %
179 % } else {
180 % # image and download links should use the cached data
181 % # just directly reference this component
182 % my $myself = $p.'graph/elements/report.html?session='.$session;
183 %
184 <% include('/elements/header.html', $opt{'title'} ) %>
185 % unless ( $opt{'graph_type'} eq 'none' ) {
186
187 <IMG SRC="<% "$myself;_type=png" %>" WIDTH="976" HEIGHT="384"
188  STYLE="page-break-after:always;">
189 % }
190 <P ALIGN="right" CLASS="noprint">
191
192 % unless ( $opt{'disable_download'} ) { 
193             Download full results<BR>
194             as <A HREF="<% "$myself;_type=xls" %>">Excel spreadsheet</A><BR>
195             as <A HREF="<% "$myself;_type=csv" %>">CSV file</A></P>
196 % } 
197 %
198 </P>
199 %# indexed by item, then by entry (the element indices of @{$data[$i]}).
200 % my @cell = ();
201 % my @styles;
202 % my $num_entries = scalar(@col_labels);
203 % my $num_items = scalar(@items);
204 % $cell[0] = ['']; #top left corner
205 % foreach my $column ( @col_labels ) {
206 %   $column =~ s/ /\<BR\>/;
207 %   push @{$cell[0]}, $column;
208 % }
209 % if ( ! $opt{'nototal'} ) {
210 %   $num_entries++;
211 %   push @{$cell[0]}, emt('Total');
212 % }
213
214 % # i for item, e for entry
215 % my $i = 1;
216 % foreach my $row ( @items ) {
217 % #make a style
218 %   my $color = shift @{ $opt{'colors'} };
219 %   push @styles, ".i$i { text-align: right; color: #$color; }";
220 % #create the data row
221 %   my $links = shift @{$opt{'links'}} || [''];
222 %   my $link_prefix = shift @$links;
223 %   $link_prefix = '<A CLASS="cell" HREF="'.$link_prefix if $link_prefix;
224 %   my $label = shift @row_labels;
225 %   $cell[$i] = [ $label ];
226 %
227 %   my $data_row = $data[$i-1];
228 %#   my $data_row = shift @data;
229 %   if ( ! $opt{'nototal'} ) {
230 %     push @$data_row, sum(@$data_row);
231 %   }
232 %   foreach ( @$data_row ) {
233 %     my $entry = $_;
234 %     $entry = $money_char . sprintf($sprintf, $entry);
235 %     $entry = $link_prefix . shift(@$links) . "\">$entry</A>" if $link_prefix;
236 %     push @{$cell[$i]}, $entry;
237 %   }
238 %   $i++;
239 % }
240 % if ( $opt{'bottom_total'} ) {
241 %   # it's an extra item
242 %   $num_items++;
243 %   push @styles, ".i$i { text-align: right; background-color: #f5f6be; }";
244 %   my $links = $opt{'bottom_link'} || [];
245 %   my $link_prefix = shift @$links;
246 %   $link_prefix = '<A CLASS="cell" HREF="'.$link_prefix if $link_prefix;
247 %   $cell[$i] = [ emt('Total') ];
248 %   for (my $e = 0; $e < $num_entries + 1; $e++) {
249 %     my $entry = sum(map { $_->[$e] } @data);
250 %     $entry = $money_char . sprintf($sprintf, $entry);
251 %     $entry = $link_prefix . shift(@$links) . "\">$entry</A>" if $link_prefix;
252 %     push @{$cell[$i]}, $entry;
253 %   }
254 % }
255
256 <STYLE type="text/css">
257 a.cell {
258   color: inherit !important;
259 }
260 td.cell {
261   border-color: #000;
262 }
263 <% join("\n", @styles) %>
264 %# item labels
265 .e0 {
266   text-align: center;
267   font-weight: bold;
268 }
269 %# totals
270 % if ( ! $opt{'nototal'} ) {
271 .e<% $num_entries %> {
272   text-align: right;
273   background-color: #f5f6be;
274 }
275 % }
276 %# date labels
277 .i0 {
278   text-align: center;
279   font-weight: bold;
280 }
281 </STYLE>
282
283 <% include('/elements/table.html', 'f8f8f8') %>
284 % if ( $opt{'transpose'} ) {
285 %   for ( my $e = 0; $e < $num_entries + 1; $e++ ) {
286   <TR>
287 %     for ( my $i = 0; $i < $num_items + 1; $i++ ) {
288     <TD CLASS="<%"cell i$i e$e"%>"><% $cell[$i][$e] %></TD>
289 %     }
290   </TR>
291 %   }
292 %
293 % } else { #!transpose
294 %
295 %   for (my $i = 0; $i < $num_items + 1; $i++) {
296   <TR>
297 %     for (my $e = 0; $e < $num_entries + 1; $e++) {
298     <TD CLASS="<%"cell i$i e$e"%>"><% $cell[$i][$e] %></TD>
299 %     }
300   </TR>
301 %   }
302 </TABLE>
303 % }
304
305 <% include('/elements/footer.html') %>
306 % } 
307 <%once>
308
309 </%once>
310 <%init>
311
312 my(%opt) = @_;
313 my $session;
314 # load from cache if possible, to avoid recalculating
315 if ( $cgi->param('session') =~ /^(\d+)$/ ) {
316   $session = $1;
317   %opt = %{ $m->cache->get($session) };
318 }
319 else {
320   $session = sprintf("%010d%06d", time, int(rand(1000000)));
321   $m->cache->set($session, \%opt, '1h');
322 }
323
324 my $sprintf = $opt{'sprintf'} || '%.2f';
325
326 my $conf = new FS::Conf;
327 my $money_char = $opt{'disable_money'} ? '' : $conf->config('money_char');
328
329 my @items = @{ $opt{'items'} };
330
331 foreach my $other (qw( col_labels row_labels graph_labels axis_labels colors links )) {
332   if ( ref($opt{$other}) eq 'HASH' ) {
333     $opt{$other} = [ map $opt{$other}{$_}, @items ];
334   }
335 }
336
337 my @col_labels = @{$opt{'col_labels'}};
338 my @row_labels = @{$opt{'row_labels'}};
339 my @data       = @{$opt{'data'}};
340
341 $opt{'axis_labels'}  ||= $opt{'col_labels'};
342 $opt{'graph_labels'} ||= $opt{'row_labels'};
343
344 my $filename = $cgi->url(-relative => 1);
345 $filename =~ s/\.(cgi|html)$//;
346
347 </%init>