diff options
author | Mark Wells <mark@freeside.biz> | 2013-02-14 19:16:27 -0800 |
---|---|---|
committer | Mark Wells <mark@freeside.biz> | 2013-02-14 19:16:27 -0800 |
commit | 6a0458ad05422b664918b0c7a18b456c022909ba (patch) | |
tree | 156af2269c5b7a2470f29c19a1dba17a0a9a9310 | |
parent | 8a537c7d535a26aec3b5ae38eddc7a813a42a270 (diff) |
better UI for package report classes, #13507
-rw-r--r-- | FS/FS/AccessRight.pm | 2 | ||||
-rwxr-xr-x | httemplate/browse/part_pkg.cgi | 68 | ||||
-rw-r--r-- | httemplate/edit/bulk-part_pkg.html | 74 | ||||
-rw-r--r-- | httemplate/edit/process/bulk-part_pkg.html | 30 | ||||
-rw-r--r-- | httemplate/elements/checkbox-tristate.html | 78 | ||||
-rw-r--r-- | httemplate/search/cdr.html | 35 | ||||
-rw-r--r-- | httemplate/search/elements/checkbox-foot.html | 86 |
7 files changed, 341 insertions, 32 deletions
diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index c0bd8d163..3d58d208c 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 5b19a309b..5dee5b8d1 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 000000000..751bf7e5d --- /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 000000000..4775a9334 --- /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 000000000..4c26ed74e --- /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 642c2da5b..1b4604bbb 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 000000000..be1caab91 --- /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> |