[freeside-commits] branch FREESIDE_2_3_BRANCH updated. 7b3c18f90c6677884bd39d29f011ea59a257184b

Mark Wells mark at 420.am
Tue Feb 19 14:37:15 PST 2013


The branch, FREESIDE_2_3_BRANCH has been updated
       via  7b3c18f90c6677884bd39d29f011ea59a257184b (commit)
      from  c0fb4cbb552047079c3af5d3496fafd4f1b11c93 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 7b3c18f90c6677884bd39d29f011ea59a257184b
Author: Mark Wells <mark at freeside.biz>
Date:   Tue Feb 19 14:34:28 2013 -0800

    better UI for package report classes, #13057

diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm
index 78e6090..12201a7 100644
--- a/FS/FS/AccessRight.pm
+++ b/FS/FS/AccessRight.pm
@@ -341,6 +341,8 @@ tie my %rights, 'Tie::IxHash',
     'Edit package definitions',
     { rightname=>'Edit global package definitions', global=>1 },
   
+    'Bulk edit package definitions',
+
     'Edit billing events',
     { rightname=>'Edit global billing events', global=>1 },
 
diff --git a/httemplate/browse/part_pkg.cgi b/httemplate/browse/part_pkg.cgi
index dd5edba..f30bab4 100755
--- a/httemplate/browse/part_pkg.cgi
+++ b/httemplate/browse/part_pkg.cgi
@@ -1,6 +1,7 @@
 <% include( 'elements/browse.html',
                  'title'                 => 'Package Definitions',
                  'html_init'             => $html_init,
+                 'html_form'             => $html_form,
                  'html_posttotal'        => $html_posttotal,
                  'name'                  => 'package definitions',
                  'disableable'           => 1,
@@ -20,6 +21,8 @@
                  'fields'                => \@fields,
                  'links'                 => \@links,
                  'align'                 => $align,
+                 'html_init'             => $html_init,
+                 'html_foot'             => $html_foot,
              )
 %>
 <%init>
@@ -33,6 +36,7 @@ my $acl_edit_global = $curuser->access_right($edit_global);
 my $acl_config      = $curuser->access_right('Configuration'); #to edit services
                                                                #and agent types
                                                                #and bulk change
+my $acl_edit_bulk   = $curuser->access_right('Bulk edit package definitions');
 
 die "access denied"
   unless $acl_edit || $acl_edit_global;
@@ -119,9 +123,7 @@ $select = "
 
 ";
 
-my $html_init;
-#unless ( $cgi->param('active') ) {
-  $html_init = qq!
+my $html_init = qq!
     One or more service definitions are grouped together into a package 
     definition and given pricing information.  Customers purchase packages
     rather than purchase services directly.<BR><BR>
@@ -133,7 +135,6 @@ my $html_init;
     </FORM>
     <BR><BR>
   !;
-#}
 
 $cgi->param('dummy', 1);
 
@@ -402,6 +403,10 @@ if ( $taxclasses ) {
   $align .= 'l';
 }
 
+# make a table of report class optionnames =>  the actual 
+my %report_optionname_name = map { 'report_option_'.$_->num, $_->name }
+  qsearch('part_pkg_report_option', { disabled => '' });
+
 push @header, 'Plan options',
               'Services';
               #'Service', 'Quan', 'Primary';
@@ -412,8 +417,18 @@ push @fields,
                     if ( $part_pkg->plan ) {
 
                       my %options = $part_pkg->options;
-
-                      [ map { 
+                      # gather any options that are really report options,
+                      # convert them to their user-friendly names,
+                      # and sort them (I think?)
+                      my @report_options =
+                        sort { $a cmp $b }
+                        map { $report_optionname_name{$_} }
+                        grep { $options{$_}
+                               and exists($report_optionname_name{$_}) }
+                        keys %options;
+
+                      my @rows = (
+                        map { 
                               [
                                 { 'data'  => "$_: ",
                                   'align' => 'right',
@@ -424,11 +439,30 @@ push @fields,
                               ];
                             }
                         grep { $options{$_} =~ /\S/ } 
-                        grep { $_ !~ /^(setup|recur)_fee$/ }
+                        grep { $_ !~ /^(setup|recur)_fee$/ 
+                               and $_ !~ /^report_option_\d+$/ }
                         keys %options
-                      ];
+                      );
+                      if ( @report_options ) {
+                        push @rows,
+                          [ { 'data'  => 'Report classes',
+                              'align' => 'center',
+                              'style' => 'font-weight: bold',
+                              'colspan' => 2
+                            } ];
+                        foreach (@report_options) {
+                          push @rows, [
+                            { 'data'  => $_,
+                              'align' => 'center',
+                              'colspan' => 2
+                            }
+                          ];
+                        } # foreach @report_options
+                      } # if @report_options
+
+                      return \@rows;
 
-                    } else {
+                    } else { # should never happen...
 
                       [ map { [
                                 { 'data'  => uc($_),
@@ -506,4 +540,20 @@ $extra_count = ( $count_extra_sql ? ' AND ' : ' WHERE ' ). $extra_count
   if $extra_count;
 my $count_query = "SELECT COUNT(*) FROM part_pkg $count_extra_sql $extra_count";
 
+my $html_form = '';
+my $html_foot = '';
+if ( $acl_edit_bulk ) {
+  # insert a checkbox column
+  push @header, '';
+  push @fields, sub {
+    '<INPUT TYPE="checkbox" NAME="pkgpart" VALUE=' . $_[0]->pkgpart .'>';
+  };
+  push @links, '';
+  $align .= 'c';
+  $html_form = qq!<FORM ACTION="${p}edit/bulk-part_pkg.html" METHOD="POST">!;
+  $html_foot = include('/search/elements/checkbox-foot.html',
+      submit  => 'edit report classes', # for now it's only report classes
+  ) . '</FORM>';
+}
+
 </%init>
diff --git a/httemplate/edit/bulk-part_pkg.html b/httemplate/edit/bulk-part_pkg.html
new file mode 100644
index 0000000..751bf7e
--- /dev/null
+++ b/httemplate/edit/bulk-part_pkg.html
@@ -0,0 +1,74 @@
+<& /elements/header.html, 'Edit package report classes' &>
+%# change that title if we add any other editing controls
+
+%# this should be centralized somewhere
+<STYLE TYPE="text/css">
+.row0 { background-color: #eeeeee; }
+.row1 { background-color: #ffffff; }
+</STYLE>
+
+<FORM ACTION="process/bulk-part_pkg.html" METHOD="POST">
+<DIV>
+The following packages will be changed:<BR>
+% foreach my $pkgpart (sort keys(%part_pkg)) {
+<INPUT TYPE="hidden" NAME="pkgpart" VALUE="<% $pkgpart %>">
+<% $part_pkg{$pkgpart}->pkg_comment %><BR>
+% }
+</DIV>
+<BR>
+<& /elements/table-grid.html &>\
+<& /elements/tr-justtitle.html, value => mt('Report classes') &>
+% my $row = 0;
+% foreach my $num (sort keys %report_class) {
+  <TR CLASS="row<%$row % 2%>">
+    <TD>
+%   if ( defined $initial_state{$num} ) {
+      <& /elements/checkbox.html,
+            field => 'report_option_'.$num,
+            value => 1,
+            curr_value => $initial_state{$num}
+      &>
+%   } else {
+%     # needs to be a tristate so that you can say "don't change it"
+      <& /elements/checkbox-tristate.html, field => 'report_option_'.$num &>
+%   }
+    </TD>
+    <TD><% $report_class{$num}->name %></TD>
+  </TR>
+%   $row++;
+% }
+</TABLE>
+<BR>
+<INPUT TYPE="submit">
+</FORM>
+<& /elements/footer.html &>
+<%init>
+die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Bulk edit package definitions');
+my @pkgparts = $cgi->param('pkgpart')
+  or die "no package definitions selected";
+
+my %part_pkg = map { $_ => FS::part_pkg->by_key($_) } @pkgparts;
+my %part_pkg_option = map { $_ => { $part_pkg{$_}->options } } @pkgparts;
+my %report_class = map { $_->num => $_ }
+  qsearch('part_pkg_report_option', { disabled => '' });
+
+my %initial_state;
+foreach my $num (keys %report_class) {
+  my $yes = 0;
+  my $no = 0;
+  foreach my $option (values %part_pkg_option) {
+    if ( $option->{"report_option_$num"} ) {
+      $yes = 1;
+    } else {
+      $no = 1;
+    }
+  }
+  if ( $yes and $no ) {
+    $initial_state{$num} = undef;
+  } elsif ( $yes ) {
+    $initial_state{$num} = 1;
+  } elsif ( $no ) {
+    $initial_state{$num} = 0;
+  } # else, uh, you didn't provide any pkgparts
+}
+</%init>
diff --git a/httemplate/edit/process/bulk-part_pkg.html b/httemplate/edit/process/bulk-part_pkg.html
new file mode 100644
index 0000000..4775a93
--- /dev/null
+++ b/httemplate/edit/process/bulk-part_pkg.html
@@ -0,0 +1,30 @@
+% if ( $error ) {
+%  $cgi->param('error', $error);
+<% $cgi->redirect(popurl(3).'/edit/bulk-part_pkg.cgi?', $cgi->query_string) %>
+% } else {
+<% $cgi->redirect(popurl(3).'/browse/part_pkg.cgi') %>
+% }
+<%init>
+die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Bulk edit package definitions');
+
+my @pkgparts = $cgi->param('pkgpart')
+  or die "no package definitions selected";
+
+my %changes;
+foreach my $param (grep { /^report_option_\d+$/ } $cgi->param) {
+  if ( length($cgi->param($param)) ) {
+    if ( $cgi->param($param) == 1 ) {
+      $changes{$param} = 1;
+    } else {
+      $changes{$param} = '';
+    }
+  }
+}
+
+my $error;
+foreach my $pkgpart (@pkgparts) {
+  my $part_pkg = FS::part_pkg->by_key($pkgpart);
+  my %options = ( $part_pkg->options, %changes );
+  $error ||= $part_pkg->replace( options => \%options );
+}
+</%init>
diff --git a/httemplate/elements/checkbox-tristate.html b/httemplate/elements/checkbox-tristate.html
new file mode 100644
index 0000000..4c26ed7
--- /dev/null
+++ b/httemplate/elements/checkbox-tristate.html
@@ -0,0 +1,78 @@
+<%doc>
+A tristate checkbox (with three values: true, false, and null).
+Internally, this creates a checkbox, coupled via javascript to a hidden
+field that actually contains the value.  For now, the only values these
+can have are 1, 0, and empty.  Clicking the checkbox cycles between them.
+</%doc>
+<%shared>
+my $init = 0;
+</%shared>
+% if ( !$init ) {
+%   $init = 1;
+<SCRIPT TYPE="text/javascript">
+function tristate_onclick() {
+  var checkbox = this;
+  var input = checkbox.input;
+  if ( input.value == "" ) {
+    input.value = "0";
+    checkbox.checked = false;
+    checkbox.indeterminate = false;
+  } else if ( input.value == "0" ) {
+    input.value = "1";
+    checkbox.checked = true;
+    checkbox.indeterminate = false;
+  } else if ( input.value == "1" ) {
+    input.value = "";
+    checkbox.checked = true;
+    checkbox.indeterminate = true
+  }
+}
+
+var tristates = [];
+var tristate_boxes = [];
+window.onload = function() { // don't do this until all of the checkboxes exist
+%#  tristates = document.getElementsByClassName('tristate'); # curse you, IE8
+  var all_inputs = document.getElementsByTagName('input');
+  for (var i=0; i < all_inputs.length; i++) {
+    if ( all_inputs[i].className == 'tristate' ) {
+      tristates.push(all_inputs[i]);
+    }
+  }
+  for (var i=0; i < tristates.length; i++) {
+    tristate_boxes[i] =
+      document.getElementById('checkbox_' + tristates[i].name);
+    // make sure they can find each other
+    tristate_boxes[i].input = tristates[i];
+    tristates[i].checkbox = tristate_boxes[i];
+    // set event handler
+    tristate_boxes[i].onclick = tristate_onclick;
+    // set initial value
+    if ( tristates[i].value == "" ) {
+      tristate_boxes[i].indeterminate = true
+    }
+    if ( tristates[i].value != "0" ) {
+      tristate_boxes[i].checked = true;
+    }
+  }
+};
+</SCRIPT>
+% } # end of $init
+<INPUT TYPE="hidden" NAME="<% $opt{field} %>"
+                     ID="<% $opt{id} %>"
+                     VALUE="<% $curr_value %>"
+                     CLASS="tristate">
+<INPUT TYPE="checkbox" ID="checkbox_<%$opt{field}%>" CLASS="partial">
+<%init>
+
+my %opt = @_;
+
+# might be useful but I'm not implementing it yet
+#my $onchange = $opt{'onchange'}
+#                 ? 'onChange="'. $opt{'onchange'}. '(this)"'
+#                 : '';
+
+$opt{'id'} ||= 'hidden_'.$opt{'field'};
+my $curr_value = $opt{curr_value};
+$curr_value = undef
+  unless $curr_value eq '0' or $curr_value eq '1';
+</%init>
diff --git a/httemplate/search/477.html b/httemplate/search/477.html
index 6f5fcdf..04764c1 100755
--- a/httemplate/search/477.html
+++ b/httemplate/search/477.html
@@ -97,6 +97,11 @@ for(my $i=0; $i < scalar(@part2b_row_option); $i++) {
     &FS::Report::FCC_477::save_fcc477map("part2b_row_option_$i",$part2b_row_option[$i]);
 }
 
+my $part5_report_option = $cgi->param('part5_report_option');
+if ( $part5_report_option ) {
+  FS::Report::FCC_477::save_fcc477map('part5_report_option', $part5_report_option);
+}
+
 my $url_mangler = sub {
   my $part = shift;
   my $url = $cgi->url('-path_info' => 1, '-full' => 1);
diff --git a/httemplate/search/477partV.html b/httemplate/search/477partV.html
index 57f80e6..91345eb 100755
--- a/httemplate/search/477partV.html
+++ b/httemplate/search/477partV.html
@@ -32,8 +32,8 @@ for ( qw(agentnum magic state) ) {
 }
 $search_hash{'country'} = 'US';
 $search_hash{'classnum'} = [ $cgi->param('classnum') ];
-$search_hash{report_option} = $cgi->param('partv_report_option')
-  if $cgi->param('partv_report_option');
+$search_hash{report_option} = $cgi->param('part5_report_option')
+  if $cgi->param('part5_report_option');
 
 my $sql_query = FS::cust_pkg->search( { %search_hash,
                                         #'fcc_line'    => 1,
diff --git a/httemplate/search/cdr.html b/httemplate/search/cdr.html
index 531688a..2426d8a 100644
--- a/httemplate/search/cdr.html
+++ b/httemplate/search/cdr.html
@@ -9,26 +9,8 @@
                'fields' => \@fields,
                'links' => \@links,
                'html_form'   => qq!<FORM NAME="cdrForm" ACTION="$p/misc/cdr.cgi" METHOD="POST">!,
-               #false laziness w/queue.html
-               'html_foot' => sub {
-                                if ( $areboxes ) {
-                                  '<BR><INPUT TYPE="button" VALUE="select all" onClick="setAll(true)">'.
-                                  '<INPUT TYPE="button" VALUE="unselect all" onClick="setAll(false)">'.
-                                  qq!<BR><INPUT TYPE="submit" NAME="action" VALUE="reprocess selected" onClick="return confirm('Are you sure you want to reprocess the selected CDRs?')">!.
-                                  qq!<INPUT TYPE="submit" NAME="action" VALUE="delete selected" onClick="return confirm('Are you sure you want to delete the selected CDRs?')"><BR>!.
-                                  '<SCRIPT TYPE="text/javascript">'.
-                                  '  function setAll(setTo) { '.
-                                  '    theForm = document.cdrForm;'.
-                                  '    for (i=0,n=theForm.elements.length;i<n;i++)'.
-                                  '      if (theForm.elements[i].name.indexOf("acctid") != -1)'.
-                                  '        theForm.elements[i].checked = setTo;'.
-                                  '  }'.
-                                  '</SCRIPT>';
-                                } else {
-                                  '';
-                                }
-                              },
-             
+               'html_foot' => $html_foot,
+             )
 &>
 <%init>
 
@@ -44,8 +26,6 @@ my $totalminutes_sub = sub {
 
 my $conf = new FS::Conf;
 
-my $areboxes = 0;
-
 my $title = 'Call Detail Records';
 my $hashref = {};
 
@@ -344,7 +324,6 @@ my %links = (
 @fields = map { exists($fields{$_}) ? $fields{$_} : $_ } @fields;
 unshift @fields, sub {
                        return '' unless $edit_data;
-                       $areboxes = 1;
                        my $cdr = shift;
                        my $acctid = $cdr->acctid;
                        qq!<INPUT NAME="acctid$acctid" TYPE="checkbox" VALUE="1">!;
@@ -398,4 +377,14 @@ if ( $topmode ) {
     $nototalminutes = 1;
 }
 
+my $html_foot = include('/search/elements/checkbox-foot.html',
+  actions => [
+    { submit  => "reprocess selected",
+      name    => "action",
+      confirm => "Are you sure you want to reprocess the selected CDRs?" },
+    { submit  => "delete selected",
+      name    => "action",
+      confirm => "Are you sure you want to delete the selected CDRs?" },
+  ]
+);
 </%init>
diff --git a/httemplate/search/elements/checkbox-foot.html b/httemplate/search/elements/checkbox-foot.html
new file mode 100644
index 0000000..be1caab
--- /dev/null
+++ b/httemplate/search/elements/checkbox-foot.html
@@ -0,0 +1,86 @@
+<%doc>
+<& /elements/search.html,
+  # options...
+  html_foot => include('elements/checkbox-foot.html',
+                  actions => [
+                    { label   => 'Edit selected packages',
+                      action  => 'popup_package_edit()',
+                    },
+                    { submit  => 'Delete selected packages',
+                      confirm => 'Really delete these packages?'
+                    },
+                  ],
+                  filter        => '.name = "pkgpart"', # see below
+               ),
+&>
+
+This creates a footer for a search page containing a column of checkboxes.
+Typically this is used to select several items from the search result and 
+apply some change to all of them at once.  The footer always provides 
+"select all" and "unselect all" buttons.
+
+"actions" is an arrayref of action buttons to show.  Each element of the
+array is a hashref of either:
+
+- "submit" and, optionally, "confirm".  Creates a submit button.  The value 
+of "submit" becomes the "value" property of the button (and thus its label).
+If "confirm" is specified, the button will have an onclick handler that 
+displays the value of "confirm" in a popup message box and asks the user to 
+confirm the choice.
+
+- "onclick" and "label".  Creates a non-submit button that executes the 
+Javascript code in "onclick".  "label" is used as the text of the button.
+
+If you want only a single action, you can forget the arrayref-of-hashrefs
+business and just put "submit" and "confirm" (or "onclick" and "label") 
+elements in the argument list.
+
+"filter" is a javascript expression to limit which checkboxes are included in
+the "select/unselect all" actions.  By default, any input with type="checkbox"
+will be included.  If this option is given, it will be evaluated with the 
+HTML node in a variable named "obj".  The expression should return true or
+false.
+
+</%doc>
+<DIV ID="checkbox_footer" STYLE="display:block">
+<INPUT TYPE="button" VALUE="<% emt('select all') %>" onclick="setAll(true)">
+<INPUT TYPE="button" VALUE="<% emt('unselect all') %>" onclick="setAll(false)">
+<BR>
+% foreach my $action (@$actions) {
+%   if ( $action->{onclick} ) {
+<INPUT TYPE="button" <% $action->{name} %> onclick="<% $opt{onclick} %>"\
+  VALUE="<% $action->{label} |h%>">
+%   } elsif ( $action->{submit} ) {
+<INPUT TYPE="submit" <% $action->{name} %> <% $action->{confirm} %>\
+  VALUE="<% $action->{submit} |h%>">
+%   } # else do nothing
+% } #foreach
+</DIV>
+<SCRIPT>
+var checkboxes = [];
+var inputs = document.getElementsByTagName('input');
+for (var i = 0; i < inputs.length; i++) {
+  var obj = inputs[i];
+  if ( obj.type == "checkbox" && <% $filter %> ) {
+    checkboxes.push(obj);
+  }
+}
+%# avoid the need for "$areboxes" late-evaluation hackery
+if ( checkboxes.length == 0 ) {
+  document.getElementById('checkbox_footer').style.display = 'none';
+}
+function setAll(setTo) {
+  for (var i = 0; i < checkboxes.length; i++) {
+    checkboxes[i].checked = setTo;
+  }
+}
+</SCRIPT>
+<%init>
+my %opt = @_;
+my $actions = $opt{'actions'} || [ \%opt ];
+foreach (@$actions) {
+  $_->{confirm} &&= qq!onclick="return confirm('! . $_->{confirm} . qq!')"!;
+  $_->{name} &&= qq!NAME="! . $_->{name} . qq!"!;
+}
+my $filter = $opt{filter} || 'true';
+</%init>
diff --git a/httemplate/search/report_477.html b/httemplate/search/report_477.html
index 24fee05..12918a1 100755
--- a/httemplate/search/report_477.html
+++ b/httemplate/search/report_477.html
@@ -228,7 +228,9 @@
                      'table'        => 'part_pkg_report_option',
                      'name_col'     => 'name',
                      'hashref'      => { 'disabled' => '' },
-                     'element_name' => 'partv_report_option',
+                     'element_name' => 'part5_report_option',
+                     'curr_value'   =>
+                            FS::Report::FCC_477::restore_fcc477map("part5_report_option"),
                  )
             %>
         </TD>

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/AccessRight.pm                          |    2 +
 httemplate/browse/part_pkg.cgi                |   68 +++++++++++++++++---
 httemplate/edit/bulk-part_pkg.html            |   74 +++++++++++++++++++++
 httemplate/edit/process/bulk-part_pkg.html    |   30 +++++++++
 httemplate/elements/checkbox-tristate.html    |   78 ++++++++++++++++++++++
 httemplate/search/477.html                    |    5 ++
 httemplate/search/477partV.html               |    4 +-
 httemplate/search/cdr.html                    |   35 ++++-------
 httemplate/search/elements/checkbox-foot.html |   86 +++++++++++++++++++++++++
 httemplate/search/report_477.html             |    4 +-
 10 files changed, 351 insertions(+), 35 deletions(-)
 create mode 100644 httemplate/edit/bulk-part_pkg.html
 create mode 100644 httemplate/edit/process/bulk-part_pkg.html
 create mode 100644 httemplate/elements/checkbox-tristate.html
 create mode 100644 httemplate/search/elements/checkbox-foot.html




More information about the freeside-commits mailing list