X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2FUI%2FWeb.pm;h=fba4a45982648be979d3c44ad93723726cf24102;hb=5192ab23dffe251a20b6aa739d39a33ee65ce518;hp=a05a667b36325b2a32fcac704db035122a49c51f;hpb=0dd05e9ff98263d2d42b419b1e278a5a3bc594b2;p=freeside.git diff --git a/FS/FS/UI/Web.pm b/FS/FS/UI/Web.pm index a05a667b3..fba4a4598 100644 --- a/FS/FS/UI/Web.pm +++ b/FS/FS/UI/Web.pm @@ -1,14 +1,21 @@ package FS::UI::Web; use strict; -use vars qw($DEBUG $me); +use vars qw($DEBUG @ISA @EXPORT_OK $me); +use Exporter; +use Carp qw( confess ); +use HTML::Entities; use FS::Conf; +use FS::Misc::DateTime qw( parse_datetime day_end ); use FS::Record qw(dbdef); -use Number::Format; +use FS::cust_main; # are sql_balance and sql_date_balance in the right module? #use vars qw(@ISA); #use FS::UI #@ISA = qw( FS::UI ); +@ISA = qw( Exporter ); + +@EXPORT_OK = qw( svc_url ); $DEBUG = 0; $me = '[FS::UID::Web]'; @@ -25,16 +32,16 @@ sub parse_beginning_ending { my $beginning = 0; if ( $cgi->param($prefix.'begin') =~ /^(\d+)$/ ) { $beginning = $1; - } elsif ( $cgi->param($prefix.'beginning') =~ /^([ 0-9\-\/]{1,64})$/ ) { - $beginning = str2time($1) || 0; + } elsif ( $cgi->param($prefix.'beginning') =~ /^([ 0-9\-\/\:]{1,64})$/ ) { + $beginning = parse_datetime($1) || 0; } my $ending = 4294967295; #2^32-1 if ( $cgi->param($prefix.'end') =~ /^(\d+)$/ ) { $ending = $1 - 1; - } elsif ( $cgi->param($prefix.'ending') =~ /^([ 0-9\-\/]{1,64})$/ ) { - #probably need an option to turn off the + 86399 - $ending = str2time($1) + 86399; + } elsif ( $cgi->param($prefix.'ending') =~ /^([ 0-9\-\/\:]{1,64})$/ ) { + $ending = parse_datetime($1); + $ending = day_end($ending) unless $ending =~ /:/; } ( $beginning, $ending ); @@ -114,6 +121,7 @@ sub svc_url { $url .= 'svcnum=' if $query =~ /^\d+(;|$)/ or $query eq ''; } + import FS::CGI 'rooturl'; #WTF! why is this necessary my $return = rooturl(). "$opt{action}/$url$query"; $return = qq!! if $opt{ahref}; @@ -128,11 +136,19 @@ sub svc_link { sub svc_label_link { my($m, $part_svc, $cust_svc) = @_ or return ''; - svc_X_link( ($cust_svc->label)[1], @_ ); + my($svc, $label, $svcdb) = $cust_svc->label; + svc_X_link( $label, @_ ); } sub svc_X_link { my ($x, $m, $part_svc, $cust_svc) = @_ or return ''; + + return $x + unless $FS::CurrentUser::CurrentUser->access_right('View customer services'); + + confess "svc_X_link called without a service ($x, $m, $part_svc, $cust_svc)\n" + unless $cust_svc; + my $ahref = svc_url( 'ahref' => 1, 'm' => $m, @@ -144,6 +160,15 @@ sub svc_X_link { "$ahref$x"; } +#this probably needs an ACL too... +sub svc_export_links { + my ($m, $part_svc, $cust_svc) = @_ or return ''; + + my $ahref = $cust_svc->export_links; + + join('', @$ahref); +} + sub parse_lt_gt { my($cgi, $field) = @_; @@ -159,7 +184,7 @@ sub parse_lt_gt { warn "checking for ${field}_$op field\n" if $DEBUG; - if ( $cgi->param($field."_$op") =~ /^\s*\$?\s*([\d\,\s]+(\.\d\d)?)\s*$/ ) { + if ( $cgi->param($field."_$op") =~ /^\s*\$?\s*(-?[\d\,\s]+(\.\d\d)?)\s*$/ ) { my $num = $1; $num =~ s/[\,\s]+//g; @@ -176,42 +201,6 @@ sub parse_lt_gt { } -sub bytecount_unexact { - my $bc = shift; - return("$bc bytes") - if ($bc < 1000); - return(sprintf("%.2f Kbytes", $bc/1000)) - if ($bc < 1000000); - return(sprintf("%.2f Mbytes", $bc/1000000)) - if ($bc < 1000000000); - return(sprintf("%.2f Gbytes", $bc/1000000000)); -} - -sub parse_bytecount { - my $bc = shift; - return $bc if (($bc =~ tr/.//) > 1); - $bc =~ /^\s*([\d.]*)\s*([kKmMgGtT]?)[bB]?\s*$/ or return $bc; - my $base = $1; - return $bc unless length $base; - my $exponent = index ' kmgt', lc($2); - return $bc if ($exponent < 0 && $2); - $exponent = 0 if ($exponent < 0); - return $base * 1024 ** $exponent; -} - -sub display_bytecount { - my $bc = shift; - return $bc unless ($bc =~ /^(\d+)$/); - my $conf = new FS::Conf; - my $f = new Number::Format; - my $precision = $conf->exists('datavolume-significantdigits') - ? $conf->config('datavolume-significantdigits') - : 3; - my $unit = $conf->exists('datavolume-forcemegabytes') ? 'M' : 'A'; - - return $f->format_bytes($bc, precision => $precision, unit => $unit); -} - ### # cust_main report subroutines ### @@ -232,28 +221,55 @@ sub cust_header { warn "FS::UI:Web::cust_header called" if $DEBUG; + my $conf = new FS::Conf; + my %header2method = ( 'Customer' => 'name', 'Cust. Status' => 'ucfirst_cust_status', 'Cust#' => 'custnum', 'Name' => 'contact', 'Company' => 'company', + + # obsolete but might still be referenced in configuration '(bill) Customer' => 'name', '(service) Customer' => 'ship_name', '(bill) Name' => 'contact', '(service) Name' => 'ship_contact', '(bill) Company' => 'company', '(service) Company' => 'ship_company', - 'Address 1' => 'address1', - 'Address 2' => 'address2', - 'City' => 'city', - 'State' => 'state', - 'Zip' => 'zip', - 'Country' => 'country_full', + '(bill) Day phone' => 'daytime', + '(bill) Night phone' => 'night', + '(bill) Fax number' => 'fax', + + 'Customer' => 'name', + 'Address 1' => 'bill_address1', + 'Address 2' => 'bill_address2', + 'City' => 'bill_city', + 'State' => 'bill_state', + 'Zip' => 'bill_zip', + 'Country' => 'bill_country_full', 'Day phone' => 'daytime', # XXX should use msgcat, but how? 'Night phone' => 'night', # XXX should use msgcat, but how? + 'Mobile phone' => 'mobile', # XXX should use msgcat, but how? + 'Fax number' => 'fax', + '(bill) Address 1' => 'bill_address1', + '(bill) Address 2' => 'bill_address2', + '(bill) City' => 'bill_city', + '(bill) State' => 'bill_state', + '(bill) Zip' => 'bill_zip', + '(bill) Country' => 'bill_country_full', + '(service) Address 1' => 'ship_address1', + '(service) Address 2' => 'ship_address2', + '(service) City' => 'ship_city', + '(service) State' => 'ship_state', + '(service) Zip' => 'ship_zip', + '(service) Country' => 'ship_country_full', 'Invoicing email(s)' => 'invoicing_list_emailonly_scalar', + 'Payment Type' => 'payby', + 'Current Balance' => 'current_balance', ); + $header2method{'Cust#'} = 'display_custnum' + if $conf->exists('cust_main-default_agent_custid'); my %header2colormethod = ( 'Cust. Status' => 'cust_statuscolor', @@ -263,6 +279,7 @@ sub cust_header { ); my %header2align = ( 'Cust. Status' => 'c', + 'Cust#' => 'r', ); my $cust_fields; @@ -276,7 +293,6 @@ sub cust_header { } else { - my $conf = new FS::Conf; if ( $conf->exists('cust-fields') && $conf->config('cust-fields') =~ /^([\w\. \|\#\(\)]+):?/ ) @@ -293,7 +309,7 @@ sub cust_header { } @cust_header = split(/ \| /, $cust_fields); - @cust_fields = map { $header2method{$_} } @cust_header; + @cust_fields = map { $header2method{$_} || $_ } @cust_header; @cust_colors = map { exists $header2colormethod{$_} ? $header2colormethod{$_} : '' @@ -308,6 +324,14 @@ sub cust_header { @cust_header; } +sub cust_sort_fields { + cust_header(@_); + #inefficientish, but tiny lists and only run once per page + + map { $_ eq 'custnum' ? 'custnum' : '' } @cust_fields; + +} + =item cust_sql_fields [ CUST_FIELDS_VALUE ] Returns a list of fields for the SELECT portion of an SQL query. @@ -321,16 +345,100 @@ setting is supplied, the cust-fields configuration value. sub cust_sql_fields { my @fields = qw( last first company ); - push @fields, map "ship_$_", @fields; - push @fields, 'country'; +# push @fields, map "ship_$_", @fields; cust_header(@_); #inefficientish, but tiny lists and only run once per page - push @fields, - grep { my $field = $_; grep { $_ eq $field } @cust_fields } - qw( address1 address2 city state zip daytime night ); - map "cust_main.$_", @fields; + my @location_fields; + foreach my $field (qw( address1 address2 city state zip )) { + foreach my $pre ('bill_','ship_') { + if ( grep { $_ eq $pre.$field } @cust_fields ) { + push @location_fields, $pre.'location.'.$field.' AS '.$pre.$field; + } + } + } + foreach my $pre ('bill_','ship_') { + if ( grep { $_ eq $pre.'country_full' } @cust_fields ) { + push @location_fields, $pre.'locationnum'; + } + } + + foreach my $field (qw(daytime night mobile fax payby)) { + push @fields, $field if (grep { $_ eq $field } @cust_fields); + } + push @fields, 'agent_custid'; + + my @extra_fields = (); + if (grep { $_ eq 'current_balance' } @cust_fields) { + push @extra_fields, FS::cust_main->balance_sql . " AS current_balance"; + } + + map("cust_main.$_", @fields), @location_fields, @extra_fields; +} + +=item join_cust_main [ TABLE[.CUSTNUM] ] [ LOCATION_TABLE[.LOCATIONNUM] ] + +Returns an SQL join phrase for the FROM clause so that the fields listed +in L will be available. Currently joins to cust_main +itself, as well as cust_location (under the aliases 'bill_location' and +'ship_location') if address fields are needed. L should have +been called already. + +All of these will be left joins; if you want to exclude rows with no linked +cust_main record (or bill_location/ship_location), you can do so in the +WHERE clause. + +TABLE is the table containing the custnum field. If CUSTNUM (a field name +in that table) is specified, that field will be joined to cust_main.custnum. +Otherwise, this function will assume the field is named "custnum". If the +argument isn't present at all, the join will just say "USING (custnum)", +which might work. + +As a special case, if TABLE is 'cust_main', only the joins to cust_location +will be returned. + +LOCATION_TABLE is an optional table name to use for joining ship_location, +in case your query also includes package information and you want the +"service address" columns to reflect package addresses. + +=cut + +sub join_cust_main { + my ($cust_table, $location_table) = @_; + my ($custnum, $locationnum); + ($cust_table, $custnum) = split(/\./, $cust_table); + $custnum ||= 'custnum'; + ($location_table, $locationnum) = split(/\./, $location_table); + $locationnum ||= 'locationnum'; + + my $sql = ''; + if ( $cust_table ) { + $sql = " LEFT JOIN cust_main ON (cust_main.custnum = $cust_table.$custnum)" + unless $cust_table eq 'cust_main'; + } else { + $sql = " LEFT JOIN cust_main USING (custnum)"; + } + + if ( !@cust_fields or grep /^bill_/, @cust_fields ) { + + $sql .= ' LEFT JOIN cust_location bill_location'. + ' ON (bill_location.locationnum = cust_main.bill_locationnum)'; + + } + + if ( !@cust_fields or grep /^ship_/, @cust_fields ) { + + if (!$location_table) { + $location_table = 'cust_main'; + $locationnum = 'ship_locationnum'; + } + + $sql .= ' LEFT JOIN cust_location ship_location'. + " ON (ship_location.locationnum = $location_table.$locationnum) "; + } + + $sql; } =item cust_fields OBJECT [ CUST_FIELDS_VALUE ] @@ -346,29 +454,64 @@ setting is supplied, the cust-fields configuration value. =cut + sub cust_fields { - my $svc_x = shift; - warn "FS::UI::Web::cust_fields called for $svc_x ". + my $record = shift; + warn "FS::UI::Web::cust_fields called for $record ". "(cust_fields: @cust_fields)" if $DEBUG > 1; #cust_header(@_) unless @cust_fields; #now need to cache to keep cust_fields # #override incase we were passed as a sub - + my $seen_unlinked = 0; + map { - if ( $svc_x->custnum ) { - warn " $svc_x -> $_" - if $DEBUG > 1; - $svc_x->$_(@_); + if ( $record->custnum ) { + warn " $record -> $_" if $DEBUG > 1; + encode_entities( $record->$_(@_) ); } else { - warn " ($svc_x unlinked)" - if $DEBUG > 1; + warn " ($record unlinked)" if $DEBUG > 1; $seen_unlinked++ ? '' : '(unlinked)'; } } @cust_fields; } +=item cust_fields_subs + +Returns an array of subroutine references for returning customer field values. +This is similar to cust_fields, but returns each field's sub as a distinct +element. + +=cut + +sub cust_fields_subs { + my $unlinked_warn = 0; + return map { + my $f = $_; + if ( $unlinked_warn++ ) { + + sub { + my $record = shift; + if ( $record->custnum ) { + encode_entities( $record->$f(@_) ); + } else { + '(unlinked)' + }; + }; + + } else { + + sub { + my $record = shift; + $record->custnum ? encode_entities( $record->$f(@_) ) : ''; + }; + + } + + } @cust_fields; +} + =item cust_colors Returns an array of subroutine references (or empty strings) for returning @@ -430,6 +573,20 @@ sub cust_aligns { } } +=item is_mobile + +Utility function to determine if the client is a mobile browser. + +=cut + +sub is_mobile { + my $ua = $ENV{'HTTP_USER_AGENT'} || ''; + if ( $ua =~ /(?:hiptop|Blazer|Novarra|Vagabond|SonyEricsson|Symbian|NetFront|UP.Browser|UP.Link|Windows CE|MIDP|J2ME|DoCoMo|J-PHONE|PalmOS|PalmSource|iPhone|iPod|AvantGo|Nokia|Android|WebOS|S60|Opera Mini|Opera Mobi)/io ) { + return 1; + } + return 0; +} + ### # begin JSRPC code... ### @@ -441,10 +598,11 @@ use vars qw($DEBUG); use Carp; use Storable qw(nfreeze); use MIME::Base64; -use JSON; -use FS::UID; +use JSON::XS; +use FS::UID qw(getotaker); use FS::Record qw(qsearchs); use FS::queue; +use FS::CGI qw(rooturl); $DEBUG = 0; @@ -515,6 +673,8 @@ sub start_job { push @{$param{$field}}, $value; } } + $param{CurrentUser} = getotaker(); + $param{RootURL} = rooturl($self->{cgi}->self_url); warn "FS::UI::Web::start_job\n". join('', map { if ( ref($param{$_}) ) { @@ -535,6 +695,10 @@ sub start_job { #warn 'froze string of size '. length(nfreeze(\%param)). " for job args\n" # if $DEBUG; + # + # XXX FS::queue::insert knows how to do this. + # not changing it here because that requires changing it everywhere else, + # too, but we should eventually fix it my $error = $job->insert( '_JOB', encode_base64(nfreeze(\%param)) ); @@ -570,16 +734,20 @@ sub job_status { } my @return; - if ( $job && $job->status ne 'failed' ) { - @return = ( 'progress', $job->statustext ); + if ( $job && $job->status ne 'failed' && $job->status ne 'done' ) { + my ($progress, $action) = split ',', $job->statustext, 2; + $action ||= 'Server processing job'; + @return = ( 'progress', $progress, $action ); } elsif ( !$job ) { #handle job gone case : job successful # so close popup, redirect parent window... @return = ( 'complete' ); + } elsif ( $job->status eq 'done' ) { + @return = ( 'done', $job->statustext, '' ); } else { @return = ( 'error', $job ? $job->statustext : $jobnum ); } - objToJson(\@return); + encode_json \@return; }