[freeside-commits] branch master updated. 6a0458ad05422b664918b0c7a18b456c022909ba

Mark Wells mark at 420.am
Thu Feb 14 23:46:48 PST 2013


The branch, master has been updated
       via  6a0458ad05422b664918b0c7a18b456c022909ba (commit)
      from  8a537c7d535a26aec3b5ae38eddc7a813a42a270 (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 6a0458ad05422b664918b0c7a18b456c022909ba
Author: Mark Wells <mark at freeside.biz>
Date:   Thu Feb 14 19:16:27 2013 -0800

    better UI for package report classes, #13507

diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm
index c0bd8d1..3d58d20 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 5b19a30..5dee5b8 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,
@@ -21,6 +22,8 @@
                  'links'                 => \@links,
                  'align'                 => $align,
                  'link_field'            => 'pkgpart',
+                 'html_init'             => $html_init,
+                 'html_foot'             => $html_foot,
              )
 %>
 <%init>
@@ -34,6 +37,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;
@@ -131,9 +135,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>
@@ -145,7 +147,6 @@ my $html_init;
     </FORM>
     <BR><BR>
   !;
-#}
 
 $cgi->param('dummy', 1);
 
@@ -436,6 +437,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';
@@ -446,8 +451,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',
@@ -458,11 +473,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($_),
@@ -540,4 +574,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/cdr.html b/httemplate/search/cdr.html
index 642c2da..1b4604b 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 = {};
 
@@ -355,7 +335,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">!;
@@ -409,4 +388,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>

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

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/cdr.html                    |   35 ++++-------
 httemplate/search/elements/checkbox-foot.html |   86 +++++++++++++++++++++++++
 7 files changed, 341 insertions(+), 32 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