Optimize "Customer has a referring customer" condition, RT#74452
[freeside.git] / httemplate / misc / email-customers.html
1 <%doc>
2
3 Allows emailing one or more customers, based on a search for customers.
4 Search can be specified either through cust_main fields as cgi params, or
5 through a base64 encoded frozen hash in the 'search' cgi param.  Form allows
6 selecting an existing msg_template, or creating a custom message, and shows a
7 preview of the message before sending.  If linked to as a popup, include the
8 cgi parameter 'popup' for proper header handling.
9
10 This may also be used as an element in other pages, enabling you to provide
11 an alternate initial form while using this for search freezing/thawing and 
12 preview/send actions, with the following options:
13
14 acl - the access right to use (defaults to 'Bulk send customer notices')
15
16 form_action - the URL to submit the form to
17
18 process_url - the URL for starting the JSRPC process
19
20 title - the title of the page
21
22 no_search_fields - arrayref of additional fields that are not search parameters
23
24 alternate_form - subroutine that returns alternate html for the initial form,
25 replaces msgnum/from/subject/body/action inputs and submit button, not
26 used if an action is specified
27
28 post_search_hook - sub hook for additional processing after search has been
29 processed from cgi, gets passed options 'conf' and 'search' (a reference to
30 the unfrozen %search hash), should be used to set msgnum or
31 from/subject/body cgi params
32
33 </%doc>
34 % if ($popup) {
35 <& /elements/header-popup.html, $title &>
36 % } else {
37 <& /elements/header.html, $title &>
38 % }
39
40 <& /elements/error.html &>
41
42 <FORM NAME="OneTrueForm" ACTION="<% $form_action %>" METHOD="POST">
43 <INPUT TYPE="hidden" NAME="table" VALUE="<% $table %>">
44 %# Mixing search params with from address, subject, etc. required special-case
45 %# handling of those, risked name conflicts, and caused massive problems with 
46 %# multi-valued search params.  We are no longer in search context, so we 
47 %# pack the search into a Storable string for later use.
48 <INPUT TYPE="hidden" NAME="search" VALUE="<% encode_base64(nfreeze(\%search)) %>">
49 <INPUT TYPE="hidden" NAME="popup" VALUE="<% $popup %>">
50 <INPUT TYPE="hidden" NAME="url" VALUE="<% $url | h %>">
51 <INPUT TYPE="hidden" NAME="to_contact_classnum" VALUE="<% join(',', @contact_classnum) %>">
52
53 % if ( $cgi->param('preview') ) {
54 %   # preview mode: at this point we have a msg_template (either "real" or
55 %   # draft) and $html_body and $text_body contain the preview message.
56 %   # give the user a chance to back out (by going back to edit mode).
57
58     <FONT SIZE="+2">Preview notice</FONT>
59     <& /elements/progress-init.html,
60                  'OneTrueForm',
61                  [ qw( search table msgnum to_contact_classnum emailtovoice_contact custnum ) ],
62                  $process_url,
63                  $pdest,
64     &>
65
66     <TABLE CLASS="fsinnerbox">
67     <INPUT TYPE="hidden" NAME="msgnum" VALUE="<% $msg_template->msgnum %>">
68 %   # kludge these through hidden inputs because they're not really part
69 %   # of the template, but should be sticky during draft editing
70     <INPUT TYPE="hidden" NAME="from_name" VALUE="<% scalar($cgi->param('from_name')) |h %>">
71     <INPUT TYPE="hidden" NAME="from_addr" VALUE="<% scalar($cgi->param('from_addr')) |h %>">
72     <INPUT TYPE="hidden" NAME="emailtovoice_contact" VALUE="<% scalar($cgi->param('emailtovoice_contact')) |h %>">
73     <INPUT TYPE="hidden" NAME="custnum" VALUE="<% scalar($cgi->param('custnum')) |h %>">
74
75 %   if ( !$msg_template->disabled ) {
76       <& /elements/tr-td-label.html, 'label' => 'Template:' &>
77         <td><% $msg_template->msgname |h %></td>
78       </tr>
79 %   }
80
81       <& /elements/tr-td-label.html, 'label' => 'From:' &>
82         <td><% $from |h %></td>
83       </tr>
84
85       <& /elements/tr-td-label.html, 'label' => 'To contacts:' &>
86         <td><% join('<BR>', @contact_classname) %></td>
87       </tr>
88
89       <& /elements/tr-td-label.html, 'label' => 'Subject:' &>
90         <td><% $subject |h %></td>
91       </tr>
92
93       <TR><TD COLSPAN=2>&nbsp;</TD></TR>
94       <TR>
95         <TH ALIGN="right" VALIGN="top">Message (HTML display): </TD>
96         <TD CLASS="background" ALIGN="left"><% $html_body %></TD>
97       </TR>
98
99 %     my $text_body = HTML::FormatText->new(leftmargin=>0)->format(
100 %                       HTML::TreeBuilder->new_from_content(
101 %                         $html_body
102 %                       )
103 %                     );
104       <TR><TD COLSPAN=2>&nbsp;</TD></TR>
105       <TR>
106         <TH ALIGN="right" VALIGN="top">Message (Text display): </TD>
107         <TD CLASS="background" ALIGN="left">
108           <a href="javascript:void(0)" ID="email-message-text-view" style="color:#666666" onclick="showtext()">(view)</a>
109           <a href="javascript:void(0)" ID="email-message-text-hide" style="color:#666666; display: none;" onclick="hidetext()">(hide)</a>
110           <PRE id="email-message-text" style="display: none;"><% $text_body %></PRE>
111         </TD>
112       </TR>
113
114     </TABLE>
115
116     <SCRIPT>
117
118       function showtext() {
119         $('#email-message-text-view').css('display','none');
120         $('#email-message-text-hide').css('display','');
121         $('#email-message-text').slideDown();
122       }
123
124       function hidetext() {
125         $('#email-message-text-view').css('display','');
126         $('#email-message-text-hide').css('display','none');
127         $('#email-message-text').slideUp();
128       }
129
130       function areyousure(href) {
131         if (confirm("Send this notice to <% ($num_cust > 1) ? "$num_cust customers" : '1 customer' %> ?")) {
132           process();
133         }
134       }
135     </SCRIPT>
136
137     <BR>
138     <INPUT TYPE="submit" NAME="edit" VALUE="Edit">
139     <INPUT TYPE="button" VALUE="Send notice" onClick="areyousure()">
140
141 % } elsif ($opt{'alternate_form'}) {
142
143 <% &{$opt{'alternate_form'}}() %>
144
145 % } else {
146 %   # Edit mode.
147
148 <SCRIPT TYPE="text/javascript">
149 function toggle(obj) {
150   document.getElementById('table_no_template').style.display = (obj.value == 0) ? '' : 'none';
151 }
152
153 </SCRIPT>
154 % if ( $msg_template and $msg_template->disabled ) {
155 %   # if we've already established a draft template, don't let msgnum be changed
156     <& /elements/hidden.html,
157       field => 'msgnum',
158       curr_value => ( scalar($cgi->param('msgnum')) || ''),
159     &>
160 % } else {
161 Template: 
162     <& /elements/select-msg_template.html,
163         onchange   => 'toggle(this)',
164         curr_value => ( scalar($cgi->param('msgnum')) || ''),
165     &>
166     <BR>
167 % }
168 % # select destination contact classes
169 <TABLE CELLSPACING=0 id="send_to_contacts_table">
170 <TR>
171  <TD>Send to contacts:</TD>
172  <TD>
173    <div id="contactclassesdiv">
174      <& /elements/checkboxes.html,
175        'style'               => 'display: inline; vertical-align: top',
176        'disable_links'       => 1,
177        'names_list'          => \@optin_checkboxes,
178        'element_name_prefix' => 'contact_class_',
179        'checked_callback'    => sub {
180          # Called for each checkbox
181          # Return true to default as checked, false as unchecked
182          my($cgi, $name) = @_;
183          exists $dest_ischecked{$name};
184        },
185      &>
186    </div>
187 % if ($send_to_domain) {
188    <div>
189      <INPUT TYPE="checkbox" NAME="emailtovoice"  ID="emailtovoice" VALUE="ON" onclick="toggleDiv(this)">Email to voice
190    </div>
191    <div id="emailtovoicediv" style="display:none";>
192
193       <& /elements/select-cust_phone.html,
194                'cust_num'     => $cgi->param('custnum'),
195                'field_name'   => 'emailtovoice_contact',
196                'format'       => 'xxxxxxxxxx',
197                'phone_types'  => [ 'daytime', 'night', 'fax', 'mobile' ],
198       &>@<% $send_to_domain |h %>
199    </div>
200 % }
201  </TD>
202 % if (@active_classes) {
203 </tr>
204 <tr>
205 <TD>Contact Type:</TD>
206 <TD>
207   <div id="contactclassesdiv">
208     <& /elements/checkboxes.html,
209       'style'               => 'display: inline; vertical-align: top',
210       'disable_links'       => 1,
211       'names_list'          => \@classnum_checkboxes,
212       'element_name_prefix' => 'contact_class_',
213       'checked_callback'    => sub {
214         # Called for each checkbox
215         # Return true to default as checked, false as unchecked
216         my($cgi, $name) = @_;
217         exists $classnum_ischecked{$name};
218       },
219     &>
220   </div>
221 </TD>
222 % }
223 </TR>
224 </TABLE>
225 <BR>
226 % # if sending a one-off message, show a form to edit it
227   <TABLE BGCOLOR="#cccccc" CELLSPACING=0 WIDTH="100%" id="table_no_template">
228     <& /elements/tr-td-label.html, 'label' => 'From:' &>
229       <TD><& /elements/input-text.html,
230               'field' => 'from_name',
231               'value' => $conf->config('invoice_from_name', $agent_virt_agentnum) ||
232                          $conf->config('company_name', $agent_virt_agentnum), #?
233               'size'  => 20,
234               'curr_value' => scalar($cgi->param('from_name')),
235           &>&nbsp;&lt;\
236           <& /elements/input-text.html,
237               'field' => 'from_addr',
238               'type'  => 'email', # HTML5, woot
239               'value' => $conf->config('invoice_from', $agent_virt_agentnum),
240               'size'  => 20,
241               'curr_value' => scalar($cgi->param('from_addr')),
242           &>&gt;</TD>
243  
244     <& /elements/tr-input-text.html,
245                  'field' => 'subject',
246                  'label' => 'Subject:',
247                  'size'  => 50,
248                  'curr_value' => $subject,
249     &>
250
251     <TR>
252       <TD ALIGN="right" VALIGN="top" STYLE="padding-top:3px">Message: </TD>
253       <TD><& /elements/htmlarea.html, 
254                'field' => 'body',
255                'width' => 763,
256                'curr_value' => $body,
257           &>
258       </TD>
259     </TR>
260
261   </TABLE>
262
263   <INPUT TYPE="hidden" NAME="custnum" VALUE="<% scalar($cgi->param('custnum')) |h %>">
264   <INPUT TYPE="submit" NAME="preview" VALUE="Preview notice">
265
266 % } #end not action or alternate form
267
268 </FORM>
269
270 <SCRIPT TYPE="text/javascript">
271 function toggleDiv(obj) {
272   var box_contactclasses = document.getElementById('contactclassesdiv');
273   var box_emailtovoice = document.getElementById('emailtovoicediv');
274
275   box_emailtovoice.style.display = (box_emailtovoice.style.display == 'none') ? 'block' : 'none';
276   document.getElementById('emailtovoice_contact').options[0].selected=true;
277
278   box_contactclasses.style.display = (box_contactclasses.style.display == 'none') ? 'block' : 'none';
279 }
280 </SCRIPT>
281
282 <& /elements/footer.html &>
283
284 <%init>
285
286 my %opt = @_;
287
288 $opt{'acl'} ||= 'Bulk send customer notices';
289
290 my $email_to;
291
292 die "access denied"
293   unless $FS::CurrentUser::CurrentUser->access_right($opt{'acl'});
294
295 my $conf = FS::Conf->new;
296 my @no_search_fields = qw( table from subject html_body text_body popup url );
297
298 my $send_to_domain = $conf->config('email-to-voice_domain');
299
300 my $form_action = $opt{'form_action'} || 'email-customers.html';
301 my $process_url = $opt{'process_url'} || 'process/email-customers.html';
302 my $title = $opt{'title'} || 'Send customer notices';
303 push( @no_search_fields, @{$opt{'no_search_fields'}} ) if $opt{'no_search_fields'};
304
305 $m->comp('/elements/handle_uri_query');
306
307 my $table = $cgi->param('table') or die "'table' required";
308 my $agent_virt_agentnum = $cgi->param('agent_virt_agentnum') || '';
309
310 my $popup = $cgi->param('popup');
311 my $url   = $cgi->param('url');
312 my $pdest = { 'message' => "Notice sent" };
313 $pdest->{'url'} = $cgi->param('url') if $url;
314
315 my %search;
316 if ( $cgi->param('search') ) {
317   %search = %{ thaw(decode_base64( $cgi->param('search') )) };
318 }
319 else {
320   %search = $cgi->Vars;
321   delete $search{$_} for @no_search_fields;
322   # FS::$table->search is expected to know which parameters might be 
323   # multi-valued, and to accept scalar values for them also.  No good 
324   # solution to this since CGI can't tell whether a parameter _might_
325   # have had multiple values, only whether it does.
326   @search{keys %search} = map { /\0/ ? [ split /\0/, $_ ] : $_ } values %search;
327 }
328
329 &{$opt{'post_search_hook'}}(
330   'conf'   => $conf,
331   'search' => \%search,
332 ) if $opt{'post_search_hook'};
333
334 my $num_cust;
335 my $from = '';
336 if ( $cgi->param('from') ) {
337   $from = $cgi->param('from');
338 } elsif ( $cgi->param('from_name') ) {
339   $from = ($cgi->param('from_name') . ' <' . $cgi->param('from_addr') . '>');
340 } elsif ( $cgi->param('from_addr') ) {
341   $from = $cgi->param('from_addr');
342 }
343
344 my $msg_template = '';
345 if ( $cgi->param('msgnum') =~ /^(\d+)$/ ) {
346   $msg_template = FS::msg_template->by_key($1)
347     or die "template not found: ".$cgi->param('msgnum');
348 }
349
350 my @contact_classnum;
351 my @contact_classname;
352
353 my $subject = $cgi->param('subject');
354 my $body = $cgi->param('body');
355 my ($html_body, $text_body);
356
357 if ( !$cgi->param('preview') ) {
358
359   # edit mode: initialize the fields from the saved draft, if there is one
360   if ( $msg_template and $msg_template->disabled eq 'D' ) {
361     my $content = $msg_template->content(''); # no localization on these yet
362     $subject ||= $content->subject;
363     $body ||= $content->body;
364   }
365
366 } else {
367
368   my @checked_email_dest;
369   my @checked_contact_type;
370   for ($cgi->param) {
371     if (/^contact_class_(.+)$/) {
372       my $f = $1;
373       if ($f eq 'invoice' || $f eq 'message') {
374         push @checked_email_dest, $f;
375       } elsif ( $f =~ /^\d+$/ ) {
376         push @checked_contact_type, $f;
377       }
378     }
379   }
380   $search{with_email_dest} = \@checked_email_dest if @checked_email_dest;
381   $search{with_contact_type} = \@checked_contact_type if @checked_contact_type;
382
383   my $sql_query = "FS::$table"->search(\%search);
384   my $count_query = delete($sql_query->{'count_query'});
385   my $count_sth = dbh->prepare($count_query)
386     or die "Error preparing $count_query: ". dbh->errstr;
387   $count_sth->execute
388     or die "Error executing $count_query: ". $count_sth->errstr;
389   my $count_arrayref = $count_sth->fetchrow_arrayref;
390   $num_cust = $count_arrayref->[0];
391
392   if ( !$msg_template or $msg_template->disabled eq 'D' ) {
393     # then this is a one-off template; edit it in place
394     my $subject = $cgi->param('subject') || '';
395     my $body = $cgi->param('body') || '';
396
397     # create a draft template
398     $msg_template ||= FS::msg_template->new({
399       msgclass  => 'email',
400       disabled  => 'D',
401     });
402     # anyone have a better idea for msgname?
403     $msg_template->set('msgname' => "Notice " . DateTime->now->iso8601);
404     $msg_template->set('from_addr' => $from);
405     my %content = (
406       subject => $subject,
407       body    => $body,
408     );
409     my $error;
410     if ( $msg_template->msgnum ) {
411       $error = $msg_template->replace(%content);
412     } else {
413       $error = $msg_template->insert(%content);
414     }
415
416     if ( $error ) {
417       $cgi->param('error', $error);
418       $cgi->delete('preview'); # don't go on to preview stage yet
419       undef $msg_template;
420     }
421   }
422   # unless creating the msg_template failed, we now have one, so construct a
423   # preview message from the first customer/whatever in the search results
424
425   my $cust;
426
427   if ( $msg_template ) { 
428     $sql_query->{'extra_sql'} .= ' LIMIT 1';
429     $sql_query->{'select'} = "$table.*";
430     $sql_query->{'order_by'} = '';
431     my $object = qsearchs($sql_query);
432     # Could use better error handling here...
433     die "No customers match the search criteria" unless ref $object;
434     $cust = $object->cust_main;
435     my %msgopts = (
436       'cust_main' => $cust,
437       'object' => $object,
438     );
439
440     my $cust_msg = $msg_template->prepare(%msgopts);
441     $from = $cust_msg->env_from;
442     $html_body = $cust_msg->preview;
443 #hmm.  this came in with the #37098 rewrite, but isn't on v3 :/
444 # causing problems with mangling subject of unrelated things
445 # should probably decode instead of ignore the UTF-8 thing, but
446 # this at least masks the ugliness for now :/
447     if ( $cust_msg->header =~ /^subject: (.*)/mi && $1 !~ /^\=\?UTF-8/ ) {
448       $subject = $1;
449     }
450   }
451
452   # contact_class_X params
453   #we can't switch to multi_param until we're done supporting deb 7
454   local($CGI::LIST_CONTEXT_WARN) = 0;
455
456   if ($cgi->param('emailtovoice_contact')) {
457     $email_to = $cgi->param('emailtovoice_contact') . '@' . $send_to_domain;
458     push @contact_classnum, 'emailtovoice';
459     push @contact_classname, $email_to;
460   }
461   else {
462     foreach my $param ( $cgi->param ) {
463       if ( $param =~ /^contact_class_(\w+)$/ ) {
464         push @contact_classnum, $1;
465         if ( $1 eq 'invoice' ) {
466           push @contact_classname, 'Invoice recipients';
467         } elsif ( $1 eq 'message' ) {
468           push @contact_classname, 'Message recipients';
469         } else {
470           my $contact_class = FS::contact_class->by_key($1);
471           push @contact_classname, encode_entities(
472             $contact_class ? $contact_class->classname : '(none)'
473           );
474         }
475       }
476     }
477   }
478 }
479
480 # Build data structures for "Opt In" and "Contact Type" checkboxes
481 #
482 # By default, message recipients will be selected, this is a message.
483 # By default, all Contact Types will be selected, but this may be
484 #   overridden by passing 'classnums' get/post values.  If no contact
485 #   types have been defined, the option will not be presented.
486
487 my @active_classes = qsearch(contact_class => {disabled => ''} );
488
489 my %classnum_ischecked;
490 my %dest_ischecked;
491
492 $CGI::LIST_CONTEXT_WARN = 0;
493 if ( my @in_classnums = $cgi->param('classnums') ) {
494   # Set checked boxes from form input
495   for my $v (@in_classnums) {
496
497     if ( $v =~ /^\d+$/ ) {
498       $classnum_ischecked{$v} = 1
499     } elsif ( $v =~ /^(invoice|message)$/ ) {
500       $dest_ischecked{$v} = 1;
501     }
502
503   }
504 } else {
505   # Checked boxes default values
506   $classnum_ischecked{$_->classnum} = 1 for @active_classes;
507   $classnum_ischecked{0} = 1;
508 }
509
510 # At least one destination is required
511 $dest_ischecked{message} = 1 unless %dest_ischecked;
512
513 my @optin_checkboxes = (
514   [ 'message' => { label => 'Message recipients' } ],
515   [ 'invoice' => { label => 'Invoice recipients' } ],
516 );
517 my @classnum_checkboxes = (
518   [ '0' => { label => '(None)' }],
519   map { [ $_->classnum => {label => $_->classname} ] } @active_classes,
520 );
521
522 </%init>