add more options to advanced package reporting
[freeside.git] / FS / FS / UI / Web.pm
1 package FS::UI::Web;
2
3 use strict;
4 use vars qw($DEBUG $me);
5 use FS::Conf;
6 use FS::Record qw(dbdef);
7
8 #use vars qw(@ISA);
9 #use FS::UI
10 #@ISA = qw( FS::UI );
11
12 $DEBUG = 0;
13 $me = '[FS::UID::Web]';
14
15 ###
16 # date parsing
17 ###
18
19 use Date::Parse;
20 sub parse_beginning_ending {
21   my($cgi, $prefix) = @_;
22   $prefix .= '_' if $prefix;
23
24   my $beginning = 0;
25   if ( $cgi->param($prefix.'begin') =~ /^(\d+)$/ ) {
26     $beginning = $1;
27   } elsif ( $cgi->param($prefix.'beginning') =~ /^([ 0-9\-\/]{1,64})$/ ) {
28     $beginning = str2time($1) || 0;
29   }
30
31   my $ending = 4294967295; #2^32-1
32   if ( $cgi->param($prefix.'end') =~ /^(\d+)$/ ) {
33     $ending = $1 - 1;
34   } elsif ( $cgi->param($prefix.'ending') =~ /^([ 0-9\-\/]{1,64})$/ ) {
35     #probably need an option to turn off the + 86399
36     $ending = str2time($1) + 86399;
37   }
38
39   ( $beginning, $ending );
40 }
41
42 =item svc_url
43
44 Returns a service URL, first checking to see if there is a service-specific
45 page to link to, otherwise to a generic service handling page.  Options are
46 passed as a list of name-value pairs, and include:
47
48 =over 4
49
50 =item * m - Mason request object ($m)
51
52 =item * action - The action for which to construct "edit", "view", or "search"
53
54 =item ** part_svc - Service definition (see L<FS::part_svc>)
55
56 =item ** svcdb - Service table
57
58 =item *** query - Query string
59
60 =item *** svc   - FS::cust_svc or FS::svc_* object
61
62 =item ahref - Optional flag, if set true returns <A HREF="$url"> instead of just the URL.
63
64 =back 
65
66 * Required fields
67
68 ** part_svc OR svcdb is required
69
70 *** query OR svc is required
71
72 =cut
73
74   # ##
75   # #required
76   # ##
77   #  'm'        => $m, #mason request object
78   #  'action'   => 'edit', #or 'view'
79   #
80   #  'part_svc' => $part_svc, #usual
81   #   #OR
82   #  'svcdb'    => 'svc_table',
83   #
84   #  'query'    => #optional query string
85   #                # (pass a blank string if you want a "raw" URL to add your
86   #                #  own svcnum to)
87   #   #OR
88   #  'svc'      => $svc_x, #or $cust_svc, it just needs a svcnum
89   #
90   # ##
91   # #optional
92   # ##
93   #  'ahref'    => 1, # if set true, returns <A HREF="$url">
94
95 use FS::CGI qw(rooturl);
96 sub svc_url {
97   my %opt = @_;
98
99   #? return '' unless ref($opt{part_svc});
100
101   my $svcdb = $opt{svcdb} || $opt{part_svc}->svcdb;
102   my $query = exists($opt{query}) ? $opt{query} : $opt{svc}->svcnum;
103   my $url;
104   warn "$me [svc_url] checking for /$opt{action}/$svcdb.cgi component"
105     if $DEBUG;
106   if ( $opt{m}->interp->comp_exists("/$opt{action}/$svcdb.cgi") ) {
107     $url = "$svcdb.cgi?";
108   } else {
109
110     my $generic = $opt{action} eq 'search' ? 'cust_svc' : 'svc_Common';
111
112     $url = "$generic.html?svcdb=$svcdb;";
113     $url .= 'svcnum=' if $query =~ /^\d+(;|$)/ or $query eq '';
114   }
115
116   my $return = rooturl(). "$opt{action}/$url$query";
117
118   $return = qq!<A HREF="$return">! if $opt{ahref};
119
120   $return;
121 }
122
123 sub svc_link {
124   my($m, $part_svc, $cust_svc) = @_ or return '';
125   svc_X_link( $part_svc->svc, @_ );
126 }
127
128 sub svc_label_link {
129   my($m, $part_svc, $cust_svc) = @_ or return '';
130   svc_X_link( ($cust_svc->label)[1], @_ );
131 }
132
133 sub svc_X_link {
134   my ($x, $m, $part_svc, $cust_svc) = @_ or return '';
135   my $ahref = svc_url(
136     'ahref'    => 1,
137     'm'        => $m,
138     'action'   => 'view',
139     'part_svc' => $part_svc,
140     'svc'      => $cust_svc,
141   );
142
143   "$ahref$x</A>";
144 }
145
146 sub parse_lt_gt {
147   my($cgi, $field) = @_;
148
149   my @search = ();
150
151   my %op = ( 
152     'lt' => '<',
153     'gt' => '>',
154   );
155
156   foreach my $op (keys %op) {
157
158     warn "checking for ${field}_$op field\n"
159       if $DEBUG;
160
161     if ( $cgi->param($field."_$op") =~ /^\s*\$?\s*([\d\,\s]+(\.\d\d)?)\s*$/ ) {
162
163       my $num = $1;
164       $num =~ s/[\,\s]+//g;
165       my $search = "$field $op{$op} $num";
166       push @search, $search;
167
168       warn "found ${field}_$op field; adding search element $search\n"
169         if $DEBUG;
170     }
171
172   }
173
174   @search;
175
176 }
177
178 sub bytecount_unexact {
179   my $bc = shift;
180   return("$bc bytes")
181     if ($bc < 1000);
182   return(sprintf("%.2f Kbytes", $bc/1000))
183     if ($bc < 1000000);
184   return(sprintf("%.2f Mbytes", $bc/1000000))
185     if ($bc < 1000000000);
186   return(sprintf("%.2f Gbytes", $bc/1000000000));
187 }
188
189 ###
190 # cust_main report subroutines
191 ###
192
193
194 =item cust_header [ CUST_FIELDS_VALUE ]
195
196 Returns an array of customer information headers according to the supplied
197 customer fields value, or if no value is supplied, the B<cust-fields>
198 configuration value.
199
200 =cut
201
202 use vars qw( @cust_fields @cust_colors @cust_styles @cust_aligns );
203
204 sub cust_header {
205
206   warn "FS::UI:Web::cust_header called"
207     if $DEBUG;
208
209   my %header2method = (
210     'Customer'                 => 'name',
211     'Cust. Status'             => 'ucfirst_cust_status',
212     'Cust#'                    => 'custnum',
213     'Name'                     => 'contact',
214     'Company'                  => 'company',
215     '(bill) Customer'          => 'name',
216     '(service) Customer'       => 'ship_name',
217     '(bill) Name'              => 'contact',
218     '(service) Name'           => 'ship_contact',
219     '(bill) Company'           => 'company',
220     '(service) Company'        => 'ship_company',
221     'Address 1'                => 'address1',
222     'Address 2'                => 'address2',
223     'City'                     => 'city',
224     'State'                    => 'state',
225     'Zip'                      => 'zip',
226     'Country'                  => 'country_full',
227     'Day phone'                => 'daytime', # XXX should use msgcat, but how?
228     'Night phone'              => 'night',   # XXX should use msgcat, but how?
229     'Invoicing email(s)'       => 'invoicing_list_emailonly_scalar',
230   );
231
232   my %header2colormethod = (
233     'Cust. Status' => 'cust_statuscolor',
234   );
235   my %header2style = (
236     'Cust. Status' => 'b',
237   );
238   my %header2align = (
239     'Cust. Status' => 'c',
240   );
241
242   my $cust_fields;
243   my @cust_header;
244   if ( @_ && $_[0] ) {
245
246     warn "  using supplied cust-fields override".
247           " (ignoring cust-fields config file)"
248       if $DEBUG;
249     $cust_fields = shift;
250
251   } else {
252
253     my $conf = new FS::Conf;
254     if (    $conf->exists('cust-fields')
255          && $conf->config('cust-fields') =~ /^([\w\. \|\#\(\)]+):?/
256        )
257     {
258       warn "  found cust-fields configuration value"
259         if $DEBUG;
260       $cust_fields = $1;
261     } else { 
262       warn "  no cust-fields configuration value found; using default 'Cust. Status | Customer'"
263         if $DEBUG;
264       $cust_fields = 'Cust. Status | Customer';
265     }
266   
267   }
268
269   @cust_header = split(/ \| /, $cust_fields);
270   @cust_fields = map { $header2method{$_} } @cust_header;
271   @cust_colors = map { exists $header2colormethod{$_}
272                          ? $header2colormethod{$_}
273                          : ''
274                      }
275                      @cust_header;
276   @cust_styles = map { exists $header2style{$_} ? $header2style{$_} : '' }
277                      @cust_header;
278   @cust_aligns = map { exists $header2align{$_} ? $header2align{$_} : 'l' }
279                      @cust_header;
280
281   #my $svc_x = shift;
282   @cust_header;
283 }
284
285 =item cust_sql_fields [ CUST_FIELDS_VALUE ]
286
287 Returns a list of fields for the SELECT portion of an SQL query.
288
289 As with L<the cust_header subroutine|/cust_header>, the fields returned are
290 defined by the supplied customer fields setting, or if no customer fields
291 setting is supplied, the <B>cust-fields</B> configuration value. 
292
293 =cut
294
295 sub cust_sql_fields {
296
297   my @fields = qw( last first company );
298   push @fields, map "ship_$_", @fields;
299   push @fields, 'country';
300
301   cust_header(@_);
302   #inefficientish, but tiny lists and only run once per page
303   push @fields,
304     grep { my $field = $_; grep { $_ eq $field } @cust_fields }
305          qw( address1 address2 city state zip daytime night );
306
307   map "cust_main.$_", @fields;
308 }
309
310 =item cust_fields OBJECT [ CUST_FIELDS_VALUE ]
311
312 Given an object that contains fields from cust_main (say, from a
313 JOINed search.  See httemplate/search/svc_* for examples), returns an array
314 of customer information, or "(unlinked)" if this service is not linked to a
315 customer.
316
317 As with L<the cust_header subroutine|/cust_header>, the fields returned are
318 defined by the supplied customer fields setting, or if no customer fields
319 setting is supplied, the <B>cust-fields</B> configuration value. 
320
321 =cut
322
323 sub cust_fields {
324   my $svc_x = shift;
325   warn "FS::UI::Web::cust_fields called for $svc_x ".
326        "(cust_fields: @cust_fields)"
327     if $DEBUG > 1;
328
329   #cust_header(@_) unless @cust_fields; #now need to cache to keep cust_fields
330   #                                     #override incase we were passed as a sub
331
332   my $seen_unlinked = 0;
333   map { 
334     if ( $svc_x->custnum ) {
335       warn "  $svc_x -> $_"
336         if $DEBUG > 1;
337       $svc_x->$_(@_);
338     } else {
339       warn "  ($svc_x unlinked)"
340         if $DEBUG > 1;
341       $seen_unlinked++ ? '' : '(unlinked)';
342     }
343   } @cust_fields;
344 }
345
346 =item cust_colors
347
348 Returns an array of subroutine references (or empty strings) for returning
349 customer information colors.
350
351 As with L<the cust_header subroutine|/cust_header>, the fields returned are
352 defined by the supplied customer fields setting, or if no customer fields
353 setting is supplied, the <B>cust-fields</B> configuration value. 
354
355 =cut
356
357 sub cust_colors {
358   map { 
359     my $method = $_;
360     if ( $method ) {
361       sub { shift->$method(@_) };
362     } else {
363       '';
364     }
365   } @cust_colors;
366 }
367
368 =item cust_styles
369
370 Returns an array of customer information styles.
371
372 As with L<the cust_header subroutine|/cust_header>, the fields returned are
373 defined by the supplied customer fields setting, or if no customer fields
374 setting is supplied, the <B>cust-fields</B> configuration value. 
375
376 =cut
377
378 sub cust_styles {
379   map { 
380     if ( $_ ) {
381       $_;
382     } else {
383       '';
384     }
385   } @cust_styles;
386 }
387
388 =item cust_aligns
389
390 Returns an array or scalar (depending on context) of customer information
391 alignments.
392
393 As with L<the cust_header subroutine|/cust_header>, the fields returned are
394 defined by the supplied customer fields setting, or if no customer fields
395 setting is supplied, the <B>cust-fields</B> configuration value. 
396
397 =cut
398
399 sub cust_aligns {
400   if ( wantarray ) {
401     @cust_aligns;
402   } else {
403     join('', @cust_aligns);
404   }
405 }
406
407 ###
408 # begin JSRPC code...
409 ###
410
411 package FS::UI::Web::JSRPC;
412
413 use strict;
414 use vars qw($DEBUG);
415 use Carp;
416 use Storable qw(nfreeze);
417 use MIME::Base64;
418 use JSON;
419 use FS::UID;
420 use FS::Record qw(qsearchs);
421 use FS::queue;
422
423 $DEBUG = 0;
424
425 sub new {
426         my $class = shift;
427         my $self  = {
428                 env => {},
429                 job => shift,
430                 cgi => shift,
431         };
432
433         bless $self, $class;
434
435         croak "CGI object required as second argument" unless $self->{'cgi'};
436
437         return $self;
438 }
439
440 sub process {
441
442   my $self = shift;
443
444   my $cgi = $self->{'cgi'};
445
446   # XXX this should parse JSON foo and build a proper data structure
447   my @args = $cgi->param('arg');
448
449   #work around konqueror bug!
450   @args = map { s/\x00$//; $_; } @args;
451
452   my $sub = $cgi->param('sub'); #????
453
454   warn "FS::UI::Web::JSRPC::process:\n".
455        "  cgi=$cgi\n".
456        "  sub=$sub\n".
457        "  args=".join(', ',@args)."\n"
458     if $DEBUG;
459
460   if ( $sub eq 'start_job' ) {
461
462     $self->start_job(@args);
463
464   } elsif ( $sub eq 'job_status' ) {
465
466     $self->job_status(@args);
467
468   } else {
469
470     die "unknown sub $sub";
471
472   }
473
474 }
475
476 sub start_job {
477   my $self = shift;
478
479   warn "FS::UI::Web::start_job: ". join(', ', @_) if $DEBUG;
480 #  my %param = @_;
481   my %param = ();
482   while ( @_ ) {
483     my( $field, $value ) = splice(@_, 0, 2);
484     unless ( exists( $param{$field} ) ) {
485       $param{$field} = $value;
486     } elsif ( ! ref($param{$field}) ) {
487       $param{$field} = [ $param{$field}, $value ];
488     } else {
489       push @{$param{$field}}, $value;
490     }
491   }
492   warn "FS::UI::Web::start_job\n".
493        join('', map {
494                       if ( ref($param{$_}) ) {
495                         "  $_ => [ ". join(', ', @{$param{$_}}). " ]\n";
496                       } else {
497                         "  $_ => $param{$_}\n";
498                       }
499                     } keys %param )
500     if $DEBUG;
501
502   #first get the CGI params shipped off to a job ASAP so an id can be returned
503   #to the caller
504   
505   my $job = new FS::queue { 'job' => $self->{'job'} };
506   
507   #too slow to insert all the cgi params as individual args..,?
508   #my $error = $queue->insert('_JOB', $cgi->Vars);
509   
510   #warn 'froze string of size '. length(nfreeze(\%param)). " for job args\n"
511   #  if $DEBUG;
512
513   my $error = $job->insert( '_JOB', encode_base64(nfreeze(\%param)) );
514
515   if ( $error ) {
516
517     warn "job not inserted: $error\n"
518       if $DEBUG;
519
520     $error;  #this doesn't seem to be handled well,
521              # will trigger "illegal jobnum" below?
522              # (should never be an error inserting the job, though, only thing
523              #  would be Pg f%*kage)
524   } else {
525
526     warn "job inserted successfully with jobnum ". $job->jobnum. "\n"
527       if $DEBUG;
528
529     $job->jobnum;
530   }
531   
532 }
533
534 sub job_status {
535   my( $self, $jobnum ) = @_; #$url ???
536
537   sleep 1; # XXX could use something better...
538
539   my $job;
540   if ( $jobnum =~ /^(\d+)$/ ) {
541     $job = qsearchs('queue', { 'jobnum' => $jobnum } );
542   } else {
543     die "FS::UI::Web::job_status: illegal jobnum $jobnum\n";
544   }
545
546   my @return;
547   if ( $job && $job->status ne 'failed' ) {
548     @return = ( 'progress', $job->statustext );
549   } elsif ( !$job ) { #handle job gone case : job successful
550                       # so close popup, redirect parent window...
551     @return = ( 'complete' );
552   } else {
553     @return = ( 'error', $job ? $job->statustext : $jobnum );
554   }
555
556   objToJson(\@return);
557
558 }
559
560 1;
561