RT# 83341 - added ability to sort by name in advanced customer reports
[freeside.git] / FS / FS / UI / Web.pm
1 package FS::UI::Web;
2
3 use strict;
4 use vars qw($DEBUG @ISA @EXPORT_OK $me);
5 use Exporter;
6 use Carp qw( confess );
7 use HTML::Entities;
8 use FS::Conf;
9 use FS::Misc::DateTime qw( parse_datetime day_end );
10 use FS::Record qw(dbdef);
11 use FS::cust_main;  # are sql_balance and sql_date_balance in the right module?
12
13 #use vars qw(@ISA);
14 #use FS::UI
15 #@ISA = qw( FS::UI );
16 @ISA = qw( Exporter );
17
18 @EXPORT_OK = qw( get_page_pref set_page_pref svc_url random_id );
19
20 $DEBUG = 0;
21 $me = '[FS::UID::Web]';
22
23 our $NO_RANDOM_IDS;
24
25 ###
26 # user prefs
27 ###
28
29 =item get_page_pref NAME, TABLENUM
30
31 Returns the user's page preference named NAME for the current page. If the
32 page is a view or edit page or otherwise shows a single record at a time,
33 it should use TABLENUM to link the preference to that record.
34
35 =cut
36
37 sub get_page_pref {
38   my ($prefname, $tablenum) = @_;
39
40   my $m = $HTML::Mason::Commands::m
41     or die "can't get page pref when running outside the UI";
42   # what's more useful: to tie prefs to the base_comp (usually where
43   # code is executing right now), or to the request_comp (approximately the
44   # one in the URL)? not sure.
45   $FS::CurrentUser::CurrentUser->get_page_pref( $m->request_comp->path,
46                                                 $prefname,
47                                                 $tablenum
48                                               );
49 }
50
51 =item set_page_pref NAME, TABLENUM, VALUE
52
53 Sets the user's page preference named NAME for the current page. Use TABLENUM
54 as for get_page_pref.
55
56 If VALUE is an empty string, the preference will be deleted (and
57 C<get_page_pref> will return an empty string).
58
59   my $mypref = set_page_pref('mypref', '', 100);
60
61 =cut
62
63 sub set_page_pref {
64   my ($prefname, $tablenum, $prefvalue) = @_;
65
66   my $m = $HTML::Mason::Commands::m
67     or die "can't set page pref when running outside the UI";
68   $FS::CurrentUser::CurrentUser->set_page_pref( $m->request_comp->path,
69                                                 $prefname,
70                                                 $tablenum,
71                                                 $prefvalue );
72 }
73
74 ###
75 # date parsing
76 ###
77
78 =item parse_beginning_ending CGI [, PREFIX ]
79
80 Parses a beginning/ending date range, as used on many reports. This function
81 recognizes two sets of CGI params: "begin" and "end", the integer timestamp
82 values, and "beginning" and "ending", the user-readable date fields.
83
84 If "begin" contains an integer, that's passed through as the beginning date.
85 Otherwise, "beginning" is passed to L<DateTime::Format::Natural> and turned
86 into an integer. If this fails or it doesn't have a value, zero is used as the
87 beginning date.
88
89 The same happens for "end" and "ending", except that if "ending" contains a
90 date without a time, it gets moved to the end of that day, and if there's no
91 value, the value returned is the highest unsigned 32-bit time value (some time
92 in 2037).
93
94 PREFIX is optionally a string to prepend (with '_' as a delimiter) to the form
95 field names.
96
97 =cut
98
99 use Date::Parse;
100 sub parse_beginning_ending {
101   my($cgi, $prefix) = @_;
102   $prefix .= '_' if $prefix;
103
104   my $beginning = 0;
105   if ( $cgi->param($prefix.'begin') =~ /^(\d+)$/ ) {
106     $beginning = $1;
107   } elsif ( $cgi->param($prefix.'beginning') =~ /^([ 0-9\-\/\:]{1,64})$/ ) {
108     $beginning = parse_datetime($1) || 0;
109   }
110
111   my $ending = 4294967295; #2^32-1
112   if ( $cgi->param($prefix.'end') =~ /^(\d+)$/ ) {
113     $ending = $1 - 1;
114   } elsif ( $cgi->param($prefix.'ending') =~ /^([ 0-9\-\/\:]{1,64})$/ ) {
115     $ending = parse_datetime($1);
116     $ending = day_end($ending) unless $ending =~ /:/;
117   }
118
119   ( $beginning, $ending );
120 }
121
122 =item svc_url
123
124 Returns a service URL, first checking to see if there is a service-specific
125 page to link to, otherwise to a generic service handling page.  Options are
126 passed as a list of name-value pairs, and include:
127
128 =over 4
129
130 =item * m - Mason request object ($m)
131
132 =item * action - The action for which to construct "edit", "view", or "search"
133
134 =item ** part_svc - Service definition (see L<FS::part_svc>)
135
136 =item ** svcdb - Service table
137
138 =item *** query - Query string
139
140 =item *** svc   - FS::cust_svc or FS::svc_* object
141
142 =item ahref - Optional flag, if set true returns <A HREF="$url"> instead of just the URL.
143
144 =back 
145
146 * Required fields
147
148 ** part_svc OR svcdb is required
149
150 *** query OR svc is required
151
152 =cut
153
154   # ##
155   # #required
156   # ##
157   #  'm'        => $m, #mason request object
158   #  'action'   => 'edit', #or 'view'
159   #
160   #  'part_svc' => $part_svc, #usual
161   #   #OR
162   #  'svcdb'    => 'svc_table',
163   #
164   #  'query'    => #optional query string
165   #                # (pass a blank string if you want a "raw" URL to add your
166   #                #  own svcnum to)
167   #   #OR
168   #  'svc'      => $svc_x, #or $cust_svc, it just needs a svcnum
169   #
170   # ##
171   # #optional
172   # ##
173   #  'ahref'    => 1, # if set true, returns <A HREF="$url">
174
175 use FS::CGI qw(rooturl);
176 sub svc_url {
177   my %opt = @_;
178
179   #? return '' unless ref($opt{part_svc});
180
181   my $svcdb = $opt{svcdb} || $opt{part_svc}->svcdb;
182   my $query = exists($opt{query}) ? $opt{query} : $opt{svc}->svcnum;
183   my $url;
184   warn "$me [svc_url] checking for /$opt{action}/$svcdb.cgi component"
185     if $DEBUG;
186   if ( $opt{m}->interp->comp_exists("/$opt{action}/$svcdb.cgi") ) {
187     $url = "$svcdb.cgi?";
188   } elsif ( $opt{m}->interp->comp_exists("/$opt{action}/$svcdb.html") ) {
189     $url = "$svcdb.html?";
190   } else {
191     my $generic = $opt{action} eq 'search' ? 'cust_svc' : 'svc_Common';
192
193     $url = "$generic.html?svcdb=$svcdb;";
194     $url .= 'svcnum=' if $query =~ /^\d+(;|$)/ or $query eq '';
195   }
196
197   my $return = FS::CGI::rooturl(). "$opt{action}/$url$query";
198
199   $return = qq!<A HREF="$return">! if $opt{ahref};
200
201   $return;
202 }
203
204 sub svc_link {
205   my($m, $part_svc, $cust_svc) = @_ or return '';
206   svc_X_link( $part_svc->svc, @_ );
207 }
208
209 sub svc_label_link {
210   my($m, $part_svc, $cust_svc) = @_ or return '';
211   my($svc, $label, $svcdb) = $cust_svc->label;
212   svc_X_link( $label, @_ );
213 }
214
215 sub svc_X_link {
216   my ($x, $m, $part_svc, $cust_svc) = @_ or return '';
217
218   return $x
219    unless $FS::CurrentUser::CurrentUser->access_right('View customer services');
220
221   confess "svc_X_link called without a service ($x, $m, $part_svc, $cust_svc)\n"
222     unless $cust_svc;
223
224   my $ahref = svc_url(
225     'ahref'    => 1,
226     'm'        => $m,
227     'action'   => 'view',
228     'part_svc' => $part_svc,
229     'svc'      => $cust_svc,
230   );
231
232   "$ahref$x</A>";
233 }
234
235 #this probably needs an ACL too...
236 sub svc_export_links {
237   my ($m, $part_svc, $cust_svc) = @_ or return '';
238
239   my $ahref = $cust_svc->export_links;
240
241   join('', @$ahref);
242 }
243
244 sub parse_lt_gt {
245   my($cgi, $field) = (shift, shift);
246   my $table = ( @_ && length($_[0]) ) ? shift.'.' : '';
247
248   my @search = ();
249
250   my %op = ( 
251     'lt' => '<',
252     'gt' => '>',
253   );
254
255   foreach my $op (keys %op) {
256
257     warn "checking for ${field}_$op field\n"
258       if $DEBUG;
259
260     if ( $cgi->param($field."_$op") =~ /^\s*\$?\s*(-?[\d\,\s]+(\.\d\d)?)\s*$/ ) {
261
262       my $num = $1;
263       $num =~ s/[\,\s]+//g;
264       my $search = "$table$field $op{$op} $num";
265       push @search, $search;
266
267       warn "found ${field}_$op field; adding search element $search\n"
268         if $DEBUG;
269     }
270
271   }
272
273   @search;
274
275 }
276
277 ###
278 # cust_main report subroutines
279 ###
280
281 =over 4
282
283 =item cust_header [ CUST_FIELDS_VALUE ]
284
285 Returns an array of customer information headers according to the supplied
286 customer fields value, or if no value is supplied, the B<cust-fields>
287 configuration value.
288
289 =cut
290
291 use vars qw( @cust_fields @cust_colors @cust_styles @cust_aligns );
292
293 sub cust_header {
294
295   warn "FS::UI:Web::cust_header called"
296     if $DEBUG;
297
298   my $conf = new FS::Conf;
299
300   my %header2method = (
301     'Customer'                 => 'name',
302     'Cust. Status'             => 'cust_status_label',
303     'Cust#'                    => 'display_custnum',
304     'Name'                     => 'contact',
305     'Company'                  => 'company',
306
307     # obsolete but might still be referenced in configuration
308     '(bill) Customer'          => 'name',
309     '(service) Customer'       => 'ship_name',
310     '(bill) Name'              => 'contact',
311     '(service) Name'           => 'ship_contact',
312     '(bill) Company'           => 'company',
313     '(service) Company'        => 'ship_company',
314     '(bill) Day phone'         => 'daytime',
315     '(bill) Night phone'       => 'night',
316     '(bill) Fax number'        => 'fax',
317  
318     'Customer'                 => 'name',
319     'Address 1'                => 'bill_address1',
320     'Address 2'                => 'bill_address2',
321     'City'                     => 'bill_city',
322     'State'                    => 'bill_state',
323     'Zip'                      => 'bill_zip',
324     'Country'                  => 'bill_country_full',
325     'Day phone'                => 'daytime', # XXX should use msgcat, but how?
326     'Night phone'              => 'night',   # XXX should use msgcat, but how?
327     'Mobile phone'             => 'mobile',  # XXX should use msgcat, but how?
328     'Fax number'               => 'fax',
329     '(bill) Address 1'         => 'bill_address1',
330     '(bill) Address 2'         => 'bill_address2',
331     '(bill) City'              => 'bill_city',
332     '(bill) State'             => 'bill_state',
333     '(bill) Zip'               => 'bill_zip',
334     '(bill) Country'           => 'bill_country_full',
335     '(bill) Latitude'          => 'bill_latitude',
336     '(bill) Longitude'         => 'bill_longitude',
337     '(service) Address 1'      => 'ship_address1',
338     '(service) Address 2'      => 'ship_address2',
339     '(service) City'           => 'ship_city',
340     '(service) State'          => 'ship_state',
341     '(service) Zip'            => 'ship_zip',
342     '(service) Country'        => 'ship_country_full',
343     '(service) Latitude'       => 'ship_latitude',
344     '(service) Longitude'      => 'ship_longitude',
345     'Invoicing email(s)'       => 'invoicing_list_emailonly_scalar',
346 # FS::Upgrade::upgrade_config removes this from existing cust-fields settings
347 #    'Payment Type'             => 'cust_payby',
348     'Current Balance'          => 'current_balance',
349     'Agent Cust#'              => 'agent_custid',
350     'Agent'                    => 'agent_name',
351     'Agent Cust# or Cust#'     => 'display_custnum',
352     'Advertising Source'       => 'referral',
353   );
354   $header2method{'Cust#'} = 'display_custnum'
355     if $conf->exists('cust_main-default_agent_custid');
356
357   my %header2colormethod = (
358     'Cust. Status' => 'cust_statuscolor',
359   );
360   my %header2style = (
361     'Cust. Status' => 'b',
362   );
363   my %header2align = (
364     'Cust. Status' => 'c',
365     'Cust#'        => 'r',
366   );
367
368   my $cust_fields;
369   my @cust_header;
370   if ( @_ && $_[0] ) {
371
372     warn "  using supplied cust-fields override".
373           " (ignoring cust-fields config file)"
374       if $DEBUG;
375     $cust_fields = shift;
376
377   } else {
378
379     if (    $conf->exists('cust-fields')
380          && $conf->config('cust-fields') =~ /^([\w\. \|\#\(\)]+):?/
381        )
382     {
383       warn "  found cust-fields configuration value"
384         if $DEBUG;
385       $cust_fields = $1;
386     } else { 
387       warn "  no cust-fields configuration value found; using default 'Cust. Status | Customer'"
388         if $DEBUG;
389       $cust_fields = 'Cust. Status | Customer';
390     }
391   
392   }
393
394   @cust_header = split(/ \| /, $cust_fields);
395   @cust_fields = map { $header2method{$_} || $_ } @cust_header;
396   @cust_colors = map { exists $header2colormethod{$_}
397                          ? $header2colormethod{$_}
398                          : ''
399                      }
400                      @cust_header;
401   @cust_styles = map { exists $header2style{$_} ? $header2style{$_} : '' }
402                      @cust_header;
403   @cust_aligns = map { exists $header2align{$_} ? $header2align{$_} : 'l' }
404                      @cust_header;
405
406   #my $svc_x = shift;
407   @cust_header;
408 }
409
410 sub cust_sort_fields {
411   cust_header(@_) if( @_ or !@cust_fields );
412   #inefficientish, but tiny lists and only run once per page
413
414   my @sort_fields;
415   foreach (@cust_fields) {
416     if ($_ eq "custnum") { push @sort_fields, 'custnum'; }
417     elsif ($_ eq "contact" || $_ eq "name") { push @sort_fields, '(last, first)'; }
418     elsif ($_ eq "company") { push @sort_fields, 'company'; }
419     else { push @sort_fields, ''; }
420   }
421   return @sort_fields;
422
423 }
424
425 =item cust_sql_fields [ CUST_FIELDS_VALUE ]
426
427 Returns a list of fields for the SELECT portion of an SQL query.
428
429 As with L<the cust_header subroutine|/cust_header>, the fields returned are
430 defined by the supplied customer fields setting, or if no customer fields
431 setting is supplied, the <B>cust-fields</B> configuration value. 
432
433 =cut
434
435 sub cust_sql_fields {
436
437   my @fields = qw( last first company );
438 #  push @fields, map "ship_$_", @fields;
439
440   cust_header(@_) if( @_ or !@cust_fields );
441   #inefficientish, but tiny lists and only run once per page
442
443   my @location_fields;
444   foreach my $field (qw( address1 address2 city state zip latitude longitude )) {
445     foreach my $pre ('bill_','ship_') {
446       if ( grep { $_ eq $pre.$field } @cust_fields ) {
447         push @location_fields, $pre.'location.'.$field.' AS '.$pre.$field;
448       }
449     }
450   }
451   foreach my $pre ('bill_','ship_') {
452     if ( grep { $_ eq $pre.'country_full' } @cust_fields ) {
453       push @location_fields, $pre.'locationnum';
454     }
455   }
456
457   foreach my $field (qw(daytime night mobile fax )) {
458     push @fields, $field if (grep { $_ eq $field } @cust_fields);
459   }
460   push @fields, 'agent_custid';
461
462   push @fields, 'agentnum' if grep { $_ eq 'agent_name' } @cust_fields;
463
464   my @extra_fields = ();
465   if (grep { $_ eq 'current_balance' } @cust_fields) {
466     push @extra_fields, FS::cust_main->balance_sql . " AS current_balance";
467   }
468
469   push @extra_fields, 'part_referral_x.referral AS referral'
470     if grep { $_ eq 'referral' } @cust_fields;
471
472   map("cust_main.$_", @fields), @location_fields, @extra_fields;
473 }
474
475 =item join_cust_main [ TABLE[.CUSTNUM] ] [ LOCATION_TABLE[.LOCATIONNUM] ]
476
477 Returns an SQL join phrase for the FROM clause so that the fields listed
478 in L</cust_sql_fields> will be available.  Currently joins to cust_main
479 itself, as well as cust_location (under the aliases 'bill_location' and
480 'ship_location') if address fields are needed.  L</cust_header> should have
481 been called already.
482
483 All of these will be left joins; if you want to exclude rows with no linked
484 cust_main record (or bill_location/ship_location), you can do so in the 
485 WHERE clause.
486
487 TABLE is the table containing the custnum field.  If CUSTNUM (a field name
488 in that table) is specified, that field will be joined to cust_main.custnum.
489 Otherwise, this function will assume the field is named "custnum".  If the 
490 argument isn't present at all, the join will just say "USING (custnum)", 
491 which might work.
492
493 As a special case, if TABLE is 'cust_main', only the joins to cust_location
494 will be returned.
495
496 LOCATION_TABLE is an optional table name to use for joining ship_location,
497 in case your query also includes package information and you want the 
498 "service address" columns to reflect package addresses.
499
500 =cut
501
502 sub join_cust_main {
503   my ($cust_table, $location_table) = @_;
504   my ($custnum, $locationnum);
505   ($cust_table, $custnum) = split(/\./, $cust_table);
506   $custnum ||= 'custnum';
507   ($location_table, $locationnum) = split(/\./, $location_table);
508   $locationnum ||= 'locationnum';
509
510   my $sql = '';
511   if ( $cust_table ) {
512     $sql = " LEFT JOIN cust_main ON (cust_main.custnum = $cust_table.$custnum)"
513       unless $cust_table eq 'cust_main';
514   } else {
515     $sql = " LEFT JOIN cust_main USING (custnum)";
516   }
517
518   if ( !@cust_fields or grep /^bill_/, @cust_fields ) {
519
520     $sql .= ' LEFT JOIN cust_location bill_location'.
521             ' ON (bill_location.locationnum = cust_main.bill_locationnum)';
522
523   }
524
525   if ( !@cust_fields or grep /^ship_/, @cust_fields ) {
526
527     if (!$location_table) {
528       $location_table = 'cust_main';
529       $locationnum = 'ship_locationnum';
530     }
531
532     $sql .= ' LEFT JOIN cust_location ship_location'.
533             " ON (ship_location.locationnum = $location_table.$locationnum) ";
534   }
535
536   if ( !@cust_fields or grep { $_ eq 'referral' } @cust_fields ) {
537     $sql .= ' LEFT JOIN (select refnum, referral from part_referral) AS part_referral_x ON (cust_main.refnum = part_referral_x.refnum) ';
538   }
539
540   $sql;
541 }
542
543 =item cust_fields OBJECT [ CUST_FIELDS_VALUE ]
544
545 Given an object that contains fields from cust_main (say, from a
546 JOINed search.  See httemplate/search/svc_* for examples), returns an array
547 of customer information, or "(unlinked)" if this service is not linked to a
548 customer.
549
550 As with L<the cust_header subroutine|/cust_header>, the fields returned are
551 defined by the supplied customer fields setting, or if no customer fields
552 setting is supplied, the <B>cust-fields</B> configuration value. 
553
554 =cut
555
556
557 sub cust_fields {
558   my $record = shift;
559   warn "FS::UI::Web::cust_fields called for $record ".
560        "(cust_fields: @cust_fields)"
561     if $DEBUG > 1;
562
563   #cust_header(@_) unless @cust_fields; #now need to cache to keep cust_fields
564   #                                     #override incase we were passed as a sub
565   
566   my $seen_unlinked = 0;
567
568   map { 
569     if ( $record->custnum ) {
570       warn "  $record -> $_" if $DEBUG > 1;
571       encode_entities( $record->$_(@_) );
572     } else {
573       warn "  ($record unlinked)" if $DEBUG > 1;
574       $seen_unlinked++ ? '' : '(unlinked)';
575     }
576   } @cust_fields;
577 }
578
579 =item cust_fields_subs
580
581 Returns an array of subroutine references for returning customer field values.
582 This is similar to cust_fields, but returns each field's sub as a distinct 
583 element.
584
585 =cut
586
587 sub cust_fields_subs {
588   my $unlinked_warn = 0;
589
590   return map { 
591     my $f = $_;
592     if ( $unlinked_warn++ ) {
593
594       sub {
595         my $record = shift;
596         if ( $record->custnum ) {
597           encode_entities( $record->$f(@_) );
598         } else {
599           '(unlinked)'
600         };
601       };
602
603     } else {
604
605       sub {
606         my $record = shift;
607         $record->custnum ? encode_entities( $record->$f(@_) ) : '';
608       };
609
610     }
611
612   } @cust_fields;
613 }
614
615 =item cust_colors
616
617 Returns an array of subroutine references (or empty strings) for returning
618 customer information colors.
619
620 As with L<the cust_header subroutine|/cust_header>, the fields returned are
621 defined by the supplied customer fields setting, or if no customer fields
622 setting is supplied, the <B>cust-fields</B> configuration value. 
623
624 =cut
625
626 sub cust_colors {
627   map { 
628     my $method = $_;
629     if ( $method ) {
630       sub { shift->$method(@_) };
631     } else {
632       '';
633     }
634   } @cust_colors;
635 }
636
637 =item cust_styles
638
639 Returns an array of customer information styles.
640
641 As with L<the cust_header subroutine|/cust_header>, the fields returned are
642 defined by the supplied customer fields setting, or if no customer fields
643 setting is supplied, the <B>cust-fields</B> configuration value. 
644
645 =cut
646
647 sub cust_styles {
648   map { 
649     if ( $_ ) {
650       $_;
651     } else {
652       '';
653     }
654   } @cust_styles;
655 }
656
657 =item cust_aligns
658
659 Returns an array or scalar (depending on context) of customer information
660 alignments.
661
662 As with L<the cust_header subroutine|/cust_header>, the fields returned are
663 defined by the supplied customer fields setting, or if no customer fields
664 setting is supplied, the <B>cust-fields</B> configuration value. 
665
666 =cut
667
668 sub cust_aligns {
669   if ( wantarray ) {
670     @cust_aligns;
671   } else {
672     join('', @cust_aligns);
673   }
674 }
675
676 =item cust_links
677
678 Returns an array of links to view/cust_main.cgi, for use with cust_fields.
679
680 =cut
681
682 sub cust_links {
683   my $link = [ FS::CGI::rooturl().'view/cust_main.cgi?', 'custnum' ];
684
685   return map { $_ eq 'cust_status_label' ? '' : $link }
686     @cust_fields;
687 }
688
689 =item is_mobile
690
691 Utility function to determine if the client is a mobile browser.
692
693 =cut
694
695 sub is_mobile {
696   my $ua = $ENV{'HTTP_USER_AGENT'} || '';
697   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 ) {
698     return 1;
699   }
700   return 0;
701 }
702
703 =item random_id [ DIGITS ]
704
705 Returns a random number of length DIGITS, or if unspecified, a long random 
706 identifier consisting of the timestamp, process ID, and a random number.
707 Anything in the UI that needs a random identifier should use this.
708
709 =cut
710
711 sub random_id {
712   my $digits = shift;
713   if (!defined $NO_RANDOM_IDS) {
714     my $conf = FS::Conf->new;
715     $NO_RANDOM_IDS = $conf->exists('no_random_ids') ? 1 : 0;
716     warn "TEST MODE--RANDOM ID NUMBERS DISABLED\n" if $NO_RANDOM_IDS;
717   }
718   if ( $NO_RANDOM_IDS ) {
719     if ( $digits > 0 ) {
720       return 0;
721     } else {
722       return '0000000000-0000-000000000.000000';
723     }
724   } else {
725     if ($digits > 0) {
726       return int(rand(10 ** $digits));
727     } else {
728       return time . "-$$-" . rand() * 2**32;
729     }
730   }
731 }
732
733 =back
734
735 =cut
736
737 ###
738 # begin JSRPC code...
739 ###
740
741 package FS::UI::Web::JSRPC;
742
743 use strict;
744 use vars qw($DEBUG);
745 use Carp;
746 use Storable qw(nfreeze);
747 use MIME::Base64;
748 use Cpanel::JSON::XS;
749 use FS::CurrentUser;
750 use FS::Record qw(qsearchs);
751 use FS::queue;
752 use FS::CGI qw(rooturl);
753 use FS::Report::Queued::FutureAutobill;
754
755 $DEBUG = 0;
756
757 sub new {
758         my $class = shift;
759         my $self  = {
760                 env => {},
761                 job => shift,
762                 cgi => shift,
763         };
764
765         bless $self, $class;
766
767         croak "CGI object required as second argument" unless $self->{'cgi'};
768
769         return $self;
770 }
771
772 sub process {
773
774   my $self = shift;
775
776   my $cgi = $self->{'cgi'};
777
778   # XXX this should parse JSON foo and build a proper data structure
779   my @args = $cgi->param('arg');
780
781   #work around konqueror bug!
782   @args = map { s/\x00$//; $_; } @args;
783
784   my $sub = $cgi->param('sub'); #????
785
786   warn "FS::UI::Web::JSRPC::process:\n".
787        "  cgi=$cgi\n".
788        "  sub=$sub\n".
789        "  args=".join(', ',@args)."\n"
790     if $DEBUG;
791
792   if ( $sub eq 'start_job' ) {
793
794     $self->start_job(@args);
795
796   } elsif ( $sub eq 'job_status' ) {
797
798     $self->job_status(@args);
799
800   } else {
801
802     die "unknown sub $sub";
803
804   }
805
806 }
807
808 sub start_job {
809   my $self = shift;
810
811   warn "FS::UI::Web::start_job: ". join(', ', @_) if $DEBUG;
812 #  my %param = @_;
813   my %param = ();
814   while ( @_ ) {
815     my( $field, $value ) = splice(@_, 0, 2);
816     unless ( exists( $param{$field} ) ) {
817       $param{$field} = $value;
818     } elsif ( ! ref($param{$field}) ) {
819       $param{$field} = [ $param{$field}, $value ];
820     } else {
821       push @{$param{$field}}, $value;
822     }
823   }
824   $param{CurrentUser} = $FS::CurrentUser::CurrentUser->username;
825   $param{RootURL} = rooturl($self->{cgi}->self_url);
826   warn "FS::UI::Web::start_job\n".
827        join('', map {
828                       if ( ref($param{$_}) ) {
829                         "  $_ => [ ". join(', ', @{$param{$_}}). " ]\n";
830                       } else {
831                         "  $_ => $param{$_}\n";
832                       }
833                     } keys %param )
834     if $DEBUG;
835
836   #first get the CGI params shipped off to a job ASAP so an id can be returned
837   #to the caller
838   
839   my $job = new FS::queue { 'job' => $self->{'job'} };
840   
841   #too slow to insert all the cgi params as individual args..,?
842   #my $error = $queue->insert('_JOB', $cgi->Vars);
843   
844   #rely on FS::queue smartness to freeze/encode the param hash
845
846   my $error = $job->insert( '_JOB', \%param );
847
848   if ( $error ) {
849
850     warn "job not inserted: $error\n"
851       if $DEBUG;
852
853     $error;  #this doesn't seem to be handled well,
854              # will trigger "illegal jobnum" below?
855              # (should never be an error inserting the job, though, only thing
856              #  would be Pg f%*kage)
857   } else {
858
859     warn "job inserted successfully with jobnum ". $job->jobnum. "\n"
860       if $DEBUG;
861
862     $job->jobnum;
863   }
864   
865 }
866
867 sub job_status {
868   my( $self, $jobnum ) = @_; #$url ???
869
870   sleep 1; # XXX could use something better...
871
872   my $job;
873   if ( $jobnum =~ /^(\d+)$/ ) {
874     $job = qsearchs('queue', { 'jobnum' => $jobnum } );
875   } else {
876     die "FS::UI::Web::job_status: illegal jobnum $jobnum\n";
877   }
878
879   my @return;
880   if ( $job && $job->status ne 'failed' && $job->status ne 'done' ) {
881     my ($progress, $action) = split ',', $job->statustext, 2; 
882     $action ||= 'Server processing job';
883     @return = ( 'progress', $progress, $action );
884   } elsif ( !$job ) { #handle job gone case : job successful
885                       # so close popup, redirect parent window...
886     @return = ( 'complete' );
887   } elsif ( $job->status eq 'done' ) {
888     @return = ( 'done', $job->statustext, '' );
889   } else {
890     @return = ( 'error', $job ? $job->statustext : $jobnum );
891   }
892
893   encode_json \@return;
894
895 }
896
897 1;
898