8a1d50236b24b53f2c0607e183921b9255fd5d66
[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     'Contact email(s)'         => 'contact_list_emailonly',
347     'Invoices'                 => 'contact_list_cust_invoice_only',
348     'Messages'                 => 'contact_list_cust_message_only',
349 # FS::Upgrade::upgrade_config removes this from existing cust-fields settings
350 #    'Payment Type'             => 'cust_payby',
351     'Current Balance'          => 'current_balance',
352     'Agent Cust#'              => 'agent_custid',
353     'Agent'                    => 'agent_name',
354     'Agent Cust# or Cust#'     => 'display_custnum',
355     'Advertising Source'       => 'referral',
356   );
357   $header2method{'Cust#'} = 'display_custnum'
358     if $conf->exists('cust_main-default_agent_custid');
359
360   my %header2colormethod = (
361     'Cust. Status' => 'cust_statuscolor',
362   );
363   my %header2style = (
364     'Cust. Status' => 'b',
365   );
366   my %header2align = (
367     'Cust. Status' => 'c',
368     'Cust#'        => 'r',
369   );
370
371   my $cust_fields;
372   my @cust_header;
373   if ( @_ && $_[0] ) {
374
375     warn "  using supplied cust-fields override".
376           " (ignoring cust-fields config file)"
377       if $DEBUG;
378     $cust_fields = shift;
379
380   } else {
381
382     if (    $conf->exists('cust-fields')
383          && $conf->config('cust-fields') =~ /^([\w\. \|\#\(\)]+):?/
384        )
385     {
386       warn "  found cust-fields configuration value"
387         if $DEBUG;
388       $cust_fields = $1;
389     } else { 
390       warn "  no cust-fields configuration value found; using default 'Cust. Status | Customer'"
391         if $DEBUG;
392       $cust_fields = 'Cust. Status | Customer';
393     }
394   
395   }
396
397   @cust_header = split(/ \| /, $cust_fields);
398   @cust_fields = map { $header2method{$_} || $_ } @cust_header;
399   @cust_colors = map { exists $header2colormethod{$_}
400                          ? $header2colormethod{$_}
401                          : ''
402                      }
403                      @cust_header;
404   @cust_styles = map { exists $header2style{$_} ? $header2style{$_} : '' }
405                      @cust_header;
406   @cust_aligns = map { exists $header2align{$_} ? $header2align{$_} : 'l' }
407                      @cust_header;
408
409   #my $svc_x = shift;
410   @cust_header;
411 }
412
413 sub cust_sort_fields {
414   cust_header(@_) if( @_ or !@cust_fields );
415   #inefficientish, but tiny lists and only run once per page
416
417   map { $_ eq 'custnum' ? 'custnum' : '' } @cust_fields;
418
419 }
420
421 =item cust_sql_fields [ CUST_FIELDS_VALUE ]
422
423 Returns a list of fields for the SELECT portion of an SQL query.
424
425 As with L<the cust_header subroutine|/cust_header>, the fields returned are
426 defined by the supplied customer fields setting, or if no customer fields
427 setting is supplied, the <B>cust-fields</B> configuration value. 
428
429 =cut
430
431 sub cust_sql_fields {
432
433   my @fields = qw( last first company );
434 #  push @fields, map "ship_$_", @fields;
435
436   cust_header(@_) if( @_ or !@cust_fields );
437   #inefficientish, but tiny lists and only run once per page
438
439   my @location_fields;
440   foreach my $field (qw( address1 address2 city state zip latitude longitude )) {
441     foreach my $pre ('bill_','ship_') {
442       if ( grep { $_ eq $pre.$field } @cust_fields ) {
443         push @location_fields, $pre.'location.'.$field.' AS '.$pre.$field;
444       }
445     }
446   }
447   foreach my $pre ('bill_','ship_') {
448     if ( grep { $_ eq $pre.'country_full' } @cust_fields ) {
449       push @location_fields, $pre.'locationnum';
450     }
451   }
452
453   foreach my $field (qw(daytime night mobile fax )) {
454     push @fields, $field if (grep { $_ eq $field } @cust_fields);
455   }
456   push @fields, 'agent_custid';
457
458   push @fields, 'agentnum' if grep { $_ eq 'agent_name' } @cust_fields;
459
460   my @extra_fields = ();
461   if (grep { $_ eq 'current_balance' } @cust_fields) {
462     push @extra_fields, FS::cust_main->balance_sql . " AS current_balance";
463   }
464
465   push @extra_fields, 'part_referral_x.referral AS referral'
466     if grep { $_ eq 'referral' } @cust_fields;
467
468   map("cust_main.$_", @fields), @location_fields, @extra_fields;
469 }
470
471 =item join_cust_main [ TABLE[.CUSTNUM] ] [ LOCATION_TABLE[.LOCATIONNUM] ]
472
473 Returns an SQL join phrase for the FROM clause so that the fields listed
474 in L<cust_sql_fields> will be available.  Currently joins to cust_main 
475 itself, as well as cust_location (under the aliases 'bill_location' and
476 'ship_location') if address fields are needed.  L<cust_header()> should have
477 been called already.
478
479 All of these will be left joins; if you want to exclude rows with no linked
480 cust_main record (or bill_location/ship_location), you can do so in the 
481 WHERE clause.
482
483 TABLE is the table containing the custnum field.  If CUSTNUM (a field name
484 in that table) is specified, that field will be joined to cust_main.custnum.
485 Otherwise, this function will assume the field is named "custnum".  If the 
486 argument isn't present at all, the join will just say "USING (custnum)", 
487 which might work.
488
489 As a special case, if TABLE is 'cust_main', only the joins to cust_location
490 will be returned.
491
492 LOCATION_TABLE is an optional table name to use for joining ship_location,
493 in case your query also includes package information and you want the 
494 "service address" columns to reflect package addresses.
495
496 =cut
497
498 sub join_cust_main {
499   my ($cust_table, $location_table) = @_;
500   my ($custnum, $locationnum);
501   ($cust_table, $custnum) = split(/\./, $cust_table);
502   $custnum ||= 'custnum';
503   ($location_table, $locationnum) = split(/\./, $location_table);
504   $locationnum ||= 'locationnum';
505
506   my $sql = '';
507   if ( $cust_table ) {
508     $sql = " LEFT JOIN cust_main ON (cust_main.custnum = $cust_table.$custnum)"
509       unless $cust_table eq 'cust_main';
510   } else {
511     $sql = " LEFT JOIN cust_main USING (custnum)";
512   }
513
514   if ( !@cust_fields or grep /^bill_/, @cust_fields ) {
515
516     $sql .= ' LEFT JOIN cust_location bill_location'.
517             ' ON (bill_location.locationnum = cust_main.bill_locationnum)';
518
519   }
520
521   if ( !@cust_fields or grep /^ship_/, @cust_fields ) {
522
523     if (!$location_table) {
524       $location_table = 'cust_main';
525       $locationnum = 'ship_locationnum';
526     }
527
528     $sql .= ' LEFT JOIN cust_location ship_location'.
529             " ON (ship_location.locationnum = $location_table.$locationnum) ";
530   }
531
532   if ( !@cust_fields or grep { $_ eq 'referral' } @cust_fields ) {
533     $sql .= ' LEFT JOIN (select refnum, referral from part_referral) AS part_referral_x ON (cust_main.refnum = part_referral_x.refnum) ';
534   }
535
536   $sql;
537 }
538
539 =item cust_fields OBJECT [ CUST_FIELDS_VALUE ]
540
541 Given an object that contains fields from cust_main (say, from a
542 JOINed search.  See httemplate/search/svc_* for examples), returns an array
543 of customer information, or "(unlinked)" if this service is not linked to a
544 customer.
545
546 As with L<the cust_header subroutine|/cust_header>, the fields returned are
547 defined by the supplied customer fields setting, or if no customer fields
548 setting is supplied, the <B>cust-fields</B> configuration value. 
549
550 =cut
551
552
553 sub cust_fields {
554   my $record = shift;
555   warn "FS::UI::Web::cust_fields called for $record ".
556        "(cust_fields: @cust_fields)"
557     if $DEBUG > 1;
558
559   #cust_header(@_) unless @cust_fields; #now need to cache to keep cust_fields
560   #                                     #override incase we were passed as a sub
561   
562   my $seen_unlinked = 0;
563
564   map { 
565     if ( $record->custnum ) {
566       warn "  $record -> $_" if $DEBUG > 1;
567       encode_entities( $record->$_(@_) );
568     } else {
569       warn "  ($record unlinked)" if $DEBUG > 1;
570       $seen_unlinked++ ? '' : '(unlinked)';
571     }
572   } @cust_fields;
573 }
574
575 =item cust_fields_subs
576
577 Returns an array of subroutine references for returning customer field values.
578 This is similar to cust_fields, but returns each field's sub as a distinct 
579 element.
580
581 =cut
582
583 sub cust_fields_subs {
584   my $unlinked_warn = 0;
585
586   return map { 
587     my $f = $_;
588     if ( $unlinked_warn++ ) {
589
590       sub {
591         my $record = shift;
592         if ( $record->custnum ) {
593           encode_entities( $record->$f(@_) );
594         } else {
595           '(unlinked)'
596         };
597       };
598
599     } else {
600
601       sub {
602         my $record = shift;
603         $record->custnum ? encode_entities( $record->$f(@_) ) : '';
604       };
605
606     }
607
608   } @cust_fields;
609 }
610
611 =item cust_colors
612
613 Returns an array of subroutine references (or empty strings) for returning
614 customer information colors.
615
616 As with L<the cust_header subroutine|/cust_header>, the fields returned are
617 defined by the supplied customer fields setting, or if no customer fields
618 setting is supplied, the <B>cust-fields</B> configuration value. 
619
620 =cut
621
622 sub cust_colors {
623   map { 
624     my $method = $_;
625     if ( $method ) {
626       sub { shift->$method(@_) };
627     } else {
628       '';
629     }
630   } @cust_colors;
631 }
632
633 =item cust_styles
634
635 Returns an array of customer information styles.
636
637 As with L<the cust_header subroutine|/cust_header>, the fields returned are
638 defined by the supplied customer fields setting, or if no customer fields
639 setting is supplied, the <B>cust-fields</B> configuration value. 
640
641 =cut
642
643 sub cust_styles {
644   map { 
645     if ( $_ ) {
646       $_;
647     } else {
648       '';
649     }
650   } @cust_styles;
651 }
652
653 =item cust_aligns
654
655 Returns an array or scalar (depending on context) of customer information
656 alignments.
657
658 As with L<the cust_header subroutine|/cust_header>, the fields returned are
659 defined by the supplied customer fields setting, or if no customer fields
660 setting is supplied, the <B>cust-fields</B> configuration value. 
661
662 =cut
663
664 sub cust_aligns {
665   if ( wantarray ) {
666     @cust_aligns;
667   } else {
668     join('', @cust_aligns);
669   }
670 }
671
672 =item cust_links
673
674 Returns an array of links to view/cust_main.cgi, for use with cust_fields.
675
676 =cut
677
678 sub cust_links {
679   my $link = [ FS::CGI::rooturl().'view/cust_main.cgi?', 'custnum' ];
680
681   return map { $_ eq 'cust_status_label' ? '' : $link }
682     @cust_fields;
683 }
684
685 =item is_mobile
686
687 Utility function to determine if the client is a mobile browser.
688
689 =cut
690
691 sub is_mobile {
692   my $ua = $ENV{'HTTP_USER_AGENT'} || '';
693   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 ) {
694     return 1;
695   }
696   return 0;
697 }
698
699 =item random_id [ DIGITS ]
700
701 Returns a random number of length DIGITS, or if unspecified, a long random 
702 identifier consisting of the timestamp, process ID, and a random number.
703 Anything in the UI that needs a random identifier should use this.
704
705 =cut
706
707 sub random_id {
708   my $digits = shift;
709   if (!defined $NO_RANDOM_IDS) {
710     my $conf = FS::Conf->new;
711     $NO_RANDOM_IDS = $conf->exists('no_random_ids') ? 1 : 0;
712     warn "TEST MODE--RANDOM ID NUMBERS DISABLED\n" if $NO_RANDOM_IDS;
713   }
714   if ( $NO_RANDOM_IDS ) {
715     if ( $digits > 0 ) {
716       return 0;
717     } else {
718       return '0000000000-0000-000000000.000000';
719     }
720   } else {
721     if ($digits > 0) {
722       return int(rand(10 ** $digits));
723     } else {
724       return time . "-$$-" . rand() * 2**32;
725     }
726   }
727 }
728
729 =back
730
731 =cut
732
733 ###
734 # begin JSRPC code...
735 ###
736
737 package FS::UI::Web::JSRPC;
738
739 use strict;
740 use vars qw($DEBUG);
741 use Carp;
742 use Storable qw(nfreeze);
743 use MIME::Base64;
744 use Cpanel::JSON::XS;
745 use FS::CurrentUser;
746 use FS::Record qw(qsearchs);
747 use FS::queue;
748 use FS::CGI qw(rooturl);
749 use FS::Report::Queued::FutureAutobill;
750
751 $DEBUG = 0;
752
753 sub new {
754         my $class = shift;
755         my $self  = {
756                 env => {},
757                 job => shift,
758                 cgi => shift,
759         };
760
761         bless $self, $class;
762
763         croak "CGI object required as second argument" unless $self->{'cgi'};
764
765         return $self;
766 }
767
768 sub process {
769
770   my $self = shift;
771
772   my $cgi = $self->{'cgi'};
773
774   # XXX this should parse JSON foo and build a proper data structure
775   my @args = $cgi->param('arg');
776
777   #work around konqueror bug!
778   @args = map { s/\x00$//; $_; } @args;
779
780   my $sub = $cgi->param('sub'); #????
781
782   warn "FS::UI::Web::JSRPC::process:\n".
783        "  cgi=$cgi\n".
784        "  sub=$sub\n".
785        "  args=".join(', ',@args)."\n"
786     if $DEBUG;
787
788   if ( $sub eq 'start_job' ) {
789
790     $self->start_job(@args);
791
792   } elsif ( $sub eq 'job_status' ) {
793
794     $self->job_status(@args);
795
796   } else {
797
798     die "unknown sub $sub";
799
800   }
801
802 }
803
804 sub start_job {
805   my $self = shift;
806
807   warn "FS::UI::Web::start_job: ". join(', ', @_) if $DEBUG;
808 #  my %param = @_;
809   my %param = ();
810   while ( @_ ) {
811     my( $field, $value ) = splice(@_, 0, 2);
812     unless ( exists( $param{$field} ) ) {
813       $param{$field} = $value;
814     } elsif ( ! ref($param{$field}) ) {
815       $param{$field} = [ $param{$field}, $value ];
816     } else {
817       push @{$param{$field}}, $value;
818     }
819   }
820   $param{CurrentUser} = $FS::CurrentUser::CurrentUser->username;
821   $param{RootURL} = rooturl($self->{cgi}->self_url);
822   warn "FS::UI::Web::start_job\n".
823        join('', map {
824                       if ( ref($param{$_}) ) {
825                         "  $_ => [ ". join(', ', @{$param{$_}}). " ]\n";
826                       } else {
827                         "  $_ => $param{$_}\n";
828                       }
829                     } keys %param )
830     if $DEBUG;
831
832   #first get the CGI params shipped off to a job ASAP so an id can be returned
833   #to the caller
834   
835   my $job = new FS::queue { 'job' => $self->{'job'} };
836   
837   #too slow to insert all the cgi params as individual args..,?
838   #my $error = $queue->insert('_JOB', $cgi->Vars);
839   
840   #rely on FS::queue smartness to freeze/encode the param hash
841
842   my $error = $job->insert( '_JOB', \%param );
843
844   if ( $error ) {
845
846     warn "job not inserted: $error\n"
847       if $DEBUG;
848
849     $error;  #this doesn't seem to be handled well,
850              # will trigger "illegal jobnum" below?
851              # (should never be an error inserting the job, though, only thing
852              #  would be Pg f%*kage)
853   } else {
854
855     warn "job inserted successfully with jobnum ". $job->jobnum. "\n"
856       if $DEBUG;
857
858     $job->jobnum;
859   }
860   
861 }
862
863 sub job_status {
864   my( $self, $jobnum ) = @_; #$url ???
865
866   sleep 1; # XXX could use something better...
867
868   my $job;
869   if ( $jobnum =~ /^(\d+)$/ ) {
870     $job = qsearchs('queue', { 'jobnum' => $jobnum } );
871   } else {
872     die "FS::UI::Web::job_status: illegal jobnum $jobnum\n";
873   }
874
875   my @return;
876   if ( $job && $job->status ne 'failed' && $job->status ne 'done' ) {
877     my ($progress, $action) = split ',', $job->statustext, 2; 
878     $action ||= 'Server processing job';
879     @return = ( 'progress', $progress, $action );
880   } elsif ( !$job ) { #handle job gone case : job successful
881                       # so close popup, redirect parent window...
882     @return = ( 'complete' );
883   } elsif ( $job->status eq 'done' ) {
884     @return = ( 'done', $job->statustext, '' );
885   } else {
886     @return = ( 'error', $job ? $job->statustext : $jobnum );
887   }
888
889   encode_json \@return;
890
891 }
892
893 1;
894