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