commit b30b38df1b66095370ca7c9ab7f178abe5be1469
Author: Bernd Bleßmann <bernd@kivitendo-premium.de>
Date:   Fri Jul 8 01:37:07 2022 +0200

    Reports als Chart exportieren können

diff --git a/SL/Controller/CustomDataExport.pm b/SL/Controller/CustomDataExport.pm
index 6dda103e8..e7d609bb4 100644
--- a/SL/Controller/CustomDataExport.pm
+++ b/SL/Controller/CustomDataExport.pm
@@ -78,6 +78,7 @@ sub action_export_with_rep_gen {
     title                 => $self->query->name,
     allow_pdf_export      => 1,
     allow_csv_export      => 1,
+    allow_chart_export    => 1,
     attachment_basename   => $report_name,
   );
 
diff --git a/SL/Controller/Helper/ReportGenerator.pm b/SL/Controller/Helper/ReportGenerator.pm
index be9b8306d..becc19659 100644
--- a/SL/Controller/Helper/ReportGenerator.pm
+++ b/SL/Controller/Helper/ReportGenerator.pm
@@ -12,7 +12,9 @@ use SL::ReportGenerator;
 
 use Exporter 'import';
 our @EXPORT = qw(
-  action_report_generator_export_as_pdf action_report_generator_export_as_csv
+  action_report_generator_export_as_pdf
+  action_report_generator_export_as_csv
+  action_report_generator_export_as_chart
   action_report_generator_back report_generator_do
   report_generator_list_objects
 );
@@ -26,7 +28,7 @@ sub _setup_action_bar {
   for my $bar ($::request->layout->get('actionbar')) {
     $bar->add(
       action => [
-        $type eq 'pdf' ? $::locale->text('PDF export') : $::locale->text('CSV export'),
+        $type eq 'pdf' ? $::locale->text('PDF export') : $type eq 'csv' ? $::locale->text('CSV export') : $::locale->text('Chart export'),
         submit => [ '#report_generator_form', { $key => "${value}report_generator_export_as_${type}" } ],
       ],
       action => [
@@ -92,6 +94,27 @@ sub action_report_generator_export_as_csv {
   print $::form->parse_html_template('report_generator/csv_export_options', { 'HIDDEN' => \@form_values });
 }
 
+sub action_report_generator_export_as_chart {
+  my ($self) = @_;
+
+  delete $::form->{action_report_generator_export_as_chart};
+
+  if ($::form->{report_generator_chart_options_set}) {
+    $self->report_generator_do('Chart');
+    return;
+  }
+
+  my $fields      = delete $::form->{report_generator_chart_fields};
+  my @form_values = $::form->flatten_variables(grep { ($_ ne 'login') && ($_ ne 'password') } keys %{ $::form });
+
+  $::form->{title} = $::locale->text('Chart export -- options');
+
+  _setup_action_bar($self, 'chart'); # Sub not exported, therefore don't call via object.
+
+  $::form->header;
+  print $::form->parse_html_template('report_generator/chart_export_options', { 'HIDDEN' => \@form_values, fields => $fields });
+}
+
 sub action_report_generator_back {
   $_[0]->report_generator_do('HTML');
 }
diff --git a/SL/ReportGenerator.pm b/SL/ReportGenerator.pm
index ed8d45c87..45b1236eb 100644
--- a/SL/ReportGenerator.pm
+++ b/SL/ReportGenerator.pm
@@ -10,6 +10,7 @@ use Text::CSV_XS;
 use strict;
 use SL::Helper::GlAttachments qw(append_gl_pdf_attachments);
 use SL::Helper::CreatePDF     qw(merge_pdfs);
+use SL::JSON qw(to_json);
 
 # Cause locales.pl to parse these files:
 # parse_html_template('report_generator/html_report')
@@ -29,6 +30,7 @@ sub new {
     'controller_class   '   => '',
     'allow_pdf_export'      => 1,
     'allow_csv_export'      => 1,
+    'allow_chart_export'    => 1,
     'html_template'         => 'report_generator/html_report',
     'pdf_export'            => {
       'paper_size'          => 'a4',
@@ -52,6 +54,10 @@ sub new {
       'headers'             => 1,
       'encoding'            => 'UTF-8',
     },
+    'chart_export'          => {
+      'assignment_x'        => 'x',
+      'assignment_y'        => 'y1',
+    },
   };
   $self->{export}   = {
     'nextsub'       => '',
@@ -168,6 +174,8 @@ sub set_options {
       $self->{options}->{pdf_export}->{$_} = $value->{$_} for keys %{ $value };
     } elsif ($key eq 'csv_export') {
       $self->{options}->{csv_export}->{$_} = $value->{$_} for keys %{ $value };
+    } elsif ($key eq 'chart_export') {
+      $self->{options}->{chart_export}->{$_} = $value->{$_} for keys %{ $value };
     } else {
       $self->{options}->{$key} = $value;
     }
@@ -185,7 +193,7 @@ sub set_options_from_form {
     $self->{options}->{$key} = $form->{$full_key} if (defined $form->{$full_key});
   }
 
-  foreach my $format (qw(pdf csv)) {
+  foreach my $format (qw(pdf csv chart)) {
     my $opts = $self->{options}->{"${format}_export"};
     foreach my $key (keys %{ $opts }) {
       my $full_key = "report_generator_${format}_options_${key}";
@@ -255,8 +263,11 @@ sub generate_with_headers {
   } elsif ($format eq 'pdf') {
     $self->generate_pdf_content();
 
+  } elsif ($format eq 'chart') {
+    $self->generate_chart_content();
+
   } else {
-    $form->error('Incorrect usage -- unknown format (supported are HTML, CSV, PDF)');
+    $form->error('Incorrect usage -- unknown format (supported are HTML, CSV, PDF, Chart)');
   }
 }
 
@@ -401,7 +412,8 @@ sub prepare_html_content {
     'RAW_BOTTOM_INFO_TEXT' => $opts->{raw_bottom_info_text},
     'ALLOW_PDF_EXPORT'     => $allow_pdf_export,
     'ALLOW_CSV_EXPORT'     => $opts->{allow_csv_export},
-    'SHOW_EXPORT_BUTTONS'  => ($allow_pdf_export || $opts->{allow_csv_export}) && $self->{data_present},
+    'ALLOW_CHART_EXPORT'   => $opts->{allow_chart_export},
+    'SHOW_EXPORT_BUTTONS'  => ($allow_pdf_export || $opts->{allow_csv_export} || $opts->{allow_chart_export}) && $self->{data_present},
     'HEADER_ROWS'          => $header_rows,
     'NUM_COLUMNS'          => scalar @column_headers,
     'ROWS'                 => \@rows,
@@ -421,7 +433,7 @@ sub create_action_bar_actions {
   my ($self, $variables, %params) = @_;
 
   my @actions;
-  foreach my $type (qw(pdf csv)) {
+  foreach my $type (qw(pdf csv chart)) {
     next unless $variables->{"ALLOW_" . uc($type) . "_EXPORT"};
 
     my $key   = $variables->{CONTROLLER_DISPATCH} ? 'action' : 'report_generator_dispatch_to';
@@ -429,7 +441,7 @@ sub create_action_bar_actions {
     $value    = $variables->{CONTROLLER_DISPATCH} . "/${value}" if $variables->{CONTROLLER_DISPATCH};
 
     push @actions, action => [
-      $type eq 'pdf' ? $::locale->text('PDF export') : $::locale->text('CSV export'),
+      $type eq 'pdf' ? $::locale->text('PDF export') : $type eq 'csv' ? $::locale->text('CSV export') : $::locale->text('Chart export'),
       submit => [ '#report_generator_form', {(
             $key => $value,
             defined $params{action_bar_additional_submit_values}
@@ -471,7 +483,6 @@ sub generate_html_content {
   $params{action_bar} //= 1;
 
   my $variables = $self->prepare_html_content(%params);
-
   $self->setup_action_bar($variables, %params) if $params{action_bar};
 
   my $stuff  = $self->{form}->parse_html_template($self->{options}->{html_template}, $variables);
@@ -858,6 +869,58 @@ sub _generate_csv_content {
   }
 }
 
+sub generate_chart_content {
+  my ($self, %params) = @_;
+
+  $params{action_bar} //= 1;
+
+  my $opts            = $self->{options};
+
+  my $assignment_x = $opts->{chart_export}->{assignment_x};
+  my $assignment_y = $opts->{chart_export}->{assignment_y};
+
+  my @data_x;
+  my @data_y;
+  foreach my $row_set (@{ $self->{data} }) {
+    next if ('ARRAY' ne ref $row_set);
+    foreach my $row (@{ $row_set }) {
+      my $x = $row->{$assignment_x}->{data}->[0];
+      my $y = $row->{$assignment_y}->{data}->[0];
+      if ($x) {
+        push @data_x, $x;
+        push @data_y, $y//0;
+      }
+    }
+  }
+
+  my $variables = {
+    'TITLE'                => $opts->{title},
+    'TOP_INFO_TEXT'        => $self->html_format($opts->{top_info_text}),
+    'RAW_TOP_INFO_TEXT'    => $opts->{raw_top_info_text},
+    'BOTTOM_INFO_TEXT'     => $self->html_format($opts->{bottom_info_text}),
+    'RAW_BOTTOM_INFO_TEXT' => $opts->{raw_bottom_info_text},
+    'EXPORT_VARIABLE_LIST' => join(' ', @{ $self->{export}->{variable_list} }),
+    'EXPORT_NEXTSUB'       => $self->{export}->{nextsub},
+    'DATA_PRESENT'         => $self->{data_present},
+    'CONTROLLER_DISPATCH'  => $opts->{controller_class},
+    'TABLE_CLASS'          => $opts->{table_class},
+    'SKIP_BUTTONS'         => !!$params{action_bar},
+  };
+
+  $::request->layout->add_javascripts('chart.js', 'kivi.ChartReport.js');
+
+  $::form->header;
+  print $::form->parse_html_template('report_generator/chart_report',
+                                      {
+                                        data_x => to_json(\@data_x),
+                                        data_y => to_json(\@data_y),
+                                        label_x => $assignment_x,
+                                        label_y => $assignment_y,
+                                        %$variables,
+                                      }
+  );
+}
+
 sub check_for_pdf_api {
   return eval { require PDF::API2; 1; } ? 1 : 0;
 }
diff --git a/bin/mozilla/reportgenerator.pl b/bin/mozilla/reportgenerator.pl
index f2eb1ef1d..caa845993 100644
--- a/bin/mozilla/reportgenerator.pl
+++ b/bin/mozilla/reportgenerator.pl
@@ -41,7 +41,7 @@ sub report_generator_setup_action_bar {
     $bar->add(
       combobox => [
         action => [
-          $type eq 'pdf' ? $::locale->text('PDF export') : $::locale->text('CSV export'),
+          $type eq 'pdf' ? $::locale->text('PDF export') : $type eq 'csv' ? $::locale->text('CSV export') : $::locale->text('Chart export'),
           submit => [ '#report_generator_form', { 'report_generator_dispatch_to' => "report_generator_export_as_${type}" } ],
         ],
         action => [
@@ -126,6 +126,31 @@ sub report_generator_export_as_csv {
   $main::lxdebug->leave_sub();
 }
 
+sub report_generator_export_as_chart {
+  $main::lxdebug->enter_sub();
+
+  my $form     = $main::form;
+  my $locale   = $main::locale;
+
+  if ($form->{report_generator_chart_options_set}) {
+    report_generator_do('Chart');
+    $main::lxdebug->leave_sub();
+    return;
+  }
+
+  my $fields      = delete $form->{report_generator_chart_fields};
+  my @form_values = $form->flatten_variables(grep { ($_ ne 'login') && ($_ ne 'password') } keys %{ $form });
+
+  $form->{title} = $locale->text('Chart export -- options');
+
+  report_generator_setup_action_bar('chart');
+
+  $form->header();
+  print $form->parse_html_template('report_generator/chart_export_options', { 'HIDDEN' => \@form_values, fields => $fields });
+
+  $main::lxdebug->leave_sub();
+}
+
 sub report_generator_back {
   $main::lxdebug->enter_sub();
 
diff --git a/js/kivi.ChartReport.js b/js/kivi.ChartReport.js
new file mode 100644
index 000000000..fcc9a9cb1
--- /dev/null
+++ b/js/kivi.ChartReport.js
@@ -0,0 +1,58 @@
+namespace('kivi.ChartReport', function(ns) {
+  "use strict";
+
+  ns.data = undefined;
+
+  ns.chart = function() {
+    $(ns.data.data_y).each(function(idx) {ns.data.data_y[idx] = kivi.parse_amount('' + ns.data.data_y[idx]);});
+    console.log("bb: data_y (parsed): "); console.log(ns.data.data_y);
+
+    const ctx = 'chart';
+    const chart = new Chart(ctx, {
+      type: 'bar',
+      data: {
+        labels: ns.data.data_x,
+        datasets: [{
+            label: ns.data.label_y,
+          data: ns.data.data_y,
+            backgroundColor: [
+                'rgba(255, 99, 132, 0.2)',
+                'rgba(54, 162, 235, 0.2)',
+                'rgba(255, 206, 86, 0.2)',
+                'rgba(75, 192, 192, 0.2)',
+                'rgba(153, 102, 255, 0.2)',
+                'rgba(255, 159, 64, 0.2)'
+            ],
+            borderColor: [
+                'rgba(255, 99, 132, 1)',
+                'rgba(54, 162, 235, 1)',
+                'rgba(255, 206, 86, 1)',
+                'rgba(75, 192, 192, 1)',
+                'rgba(153, 102, 255, 1)',
+                'rgba(255, 159, 64, 1)'
+            ],
+            borderWidth: 1
+        }]
+      },
+      options: {
+        scales: {
+          y: {
+            beginAtZero: true
+          }
+        }
+      }
+    });
+  };
+
+  ns.debug = function() {
+    console.log("bb: data_x: "); console.log(ns.data.data_x);
+    console.log("bb: data_y: "); console.log(ns.data.data_y);
+    console.log("bb: label_x: "); console.log(ns.data.label_x);
+    console.log("bb: label_y: "); console.log(ns.data.label_y);
+  };
+
+  $(function() {
+    kivi.ChartReport.debug();
+    kivi.ChartReport.chart();
+  });
+});
diff --git a/templates/webpages/report_generator/chart_export_options.html b/templates/webpages/report_generator/chart_export_options.html
new file mode 100644
index 000000000..e532f98b3
--- /dev/null
+++ b/templates/webpages/report_generator/chart_export_options.html
@@ -0,0 +1,46 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE L %]
+
+ <h1>[% HTML.escape(title) %]</h1>
+
+ <form action="[% HTML.escape(script) %]" method="post" name="report_generator_form" id="report_generator_form">
+
+  [%- FOREACH var = HIDDEN %]
+  <input type="hidden" name="[% HTML.escape(var.key) %]" value="[% HTML.escape(var.value) %]">
+  [%- END %]
+
+  <input type="hidden" name="report_generator_chart_options_set" value="1">
+  <input type="hidden" name="report_generator_dispatch_to" value="">
+
+  <table>
+   <tr>
+     <th valign="top" align="right">[% 'Chart assignments' | $T8 %]: [% 'X' | $T8 %]</th>
+     <td>
+       [% L.select_tag('report_generator_chart_options_assignment_x',
+                       fields,
+                       default = SELF.report_generator_chart_options_assignment_x
+                       value_key = 'name',
+                       title_key = 'text') %]
+     </td>
+   </tr>
+   <tr>
+     <th valign="top" align="right">[% 'Chart assignments' | $T8 %]: [% 'Y' | $T8 %]</th>
+     <td>
+       [% L.select_tag('report_generator_chart_options_assignment_y',
+                       fields,
+                       default = SELF.report_generator_chart_options_assignment_y
+                       value_key = 'name',
+                       title_key = 'text') %]
+     </td>
+   </tr>
+  </table>
+
+[%- IF CONTROLLER_DISPATCH %]
+    <input type="hidden" name="CONTROLLER_DISPATCH" value="[% CONTROLLER_DISPATCH | html %]">
+[%- ELSE %]
+   <input type="hidden" name="action" value="report_generator_dispatcher">
+[%- END %]
+
+
+ </form>
diff --git a/templates/webpages/report_generator/chart_report.html b/templates/webpages/report_generator/chart_report.html
new file mode 100644
index 000000000..5f537bf79
--- /dev/null
+++ b/templates/webpages/report_generator/chart_report.html
@@ -0,0 +1,30 @@
+[% USE P -%]
+
+<h1>[% TITLE %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+[% IF TOP_INFO_TEXT %]
+ <p>[% TOP_INFO_TEXT %]</p>
+[% END %]
+
+[% RAW_TOP_INFO_TEXT %]
+
+<div class="chart-container" style="position: relative;">
+  <canvas id="chart" height="70vH"></canvas>
+</div>
+
+[% RAW_BOTTOM_INFO_TEXT %]
+
+[% IF BOTTOM_INFO_TEXT %]
+ <p>[% BOTTOM_INFO_TEXT %]</p>
+[% END %]
+
+
+<script>
+  kivi.ChartReport.data = { data_x: [% data_x %],
+                            data_y: [% data_y %],
+                            label_y: "[% label_y %]",
+                            label_x: "[% label_x %]"
+                          };
+</script>
diff --git a/templates/webpages/report_generator/html_report.html b/templates/webpages/report_generator/html_report.html
index 4ed1ca88e..cf829c096 100644
--- a/templates/webpages/report_generator/html_report.html
+++ b/templates/webpages/report_generator/html_report.html
@@ -102,6 +102,17 @@
    [% FOREACH var = EXPORT_VARIABLES %]<input type="hidden" name="report_generator_hidden_[% var.key %]" value="[% HTML.escape(var.value) %]">
    [% END %]
 
+   [% IF ALLOW_CHART_EXPORT %]
+    [% FOREACH row = HEADER_ROWS %]
+     [% FOREACH col = row %]
+      [% IF col.name && col.text %]
+       <input type="hidden" name="report_generator_chart_fields[+].name" value="[% HTML.escape(col.name) %]">
+       <input type="hidden" name="report_generator_chart_fields[].text"  value="[% HTML.escape(col.text) %]">
+      [% END %]
+     [% END %]
+    [% END %]
+   [% END %]
+
 [%- IF CONTROLLER_DISPATCH %]
 [% IF !SKIP_BUTTONS %]
    <input type="hidden" name="action" value="[% CONTROLLER_DISPATCH %]/dispatch">
