RT# 30783 js fix for ip selection
[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'          => \@contact_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          $name eq 'message'
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 </TR>
203 </TABLE>
204 <BR>
205 % # if sending a one-off message, show a form to edit it
206   <TABLE BGCOLOR="#cccccc" CELLSPACING=0 WIDTH="100%" id="table_no_template">
207     <& /elements/tr-td-label.html, 'label' => 'From:' &>
208       <TD><& /elements/input-text.html,
209               'field' => 'from_name',
210               'value' => $conf->config('invoice_from_name', $agent_virt_agentnum) ||
211                          $conf->config('company_name', $agent_virt_agentnum), #?
212               'size'  => 20,
213               'curr_value' => scalar($cgi->param('from_name')),
214           &>&nbsp;&lt;\
215           <& /elements/input-text.html,
216               'field' => 'from_addr',
217               'type'  => 'email', # HTML5, woot
218               'value' => $conf->config('invoice_from', $agent_virt_agentnum),
219               'size'  => 20,
220               'curr_value' => scalar($cgi->param('from_addr')),
221           &>&gt;</TD>
222  
223     <& /elements/tr-input-text.html,
224                  'field' => 'subject',
225                  'label' => 'Subject:',
226                  'size'  => 50,
227                  'curr_value' => $subject,
228     &>
229
230     <TR>
231       <TD ALIGN="right" VALIGN="top" STYLE="padding-top:3px">Message: </TD>
232       <TD><& /elements/htmlarea.html, 
233                'field' => 'body',
234                'width' => 763,
235                'curr_value' => $body,
236           &>
237       </TD>
238     </TR>
239
240   </TABLE>
241
242   <INPUT TYPE="hidden" NAME="custnum" VALUE="<% scalar($cgi->param('custnum')) |h %>">
243   <INPUT TYPE="submit" NAME="preview" VALUE="Preview notice">
244
245 % } #end not action or alternate form
246
247 </FORM>
248
249 <SCRIPT TYPE="text/javascript">
250 function toggleDiv(obj) {
251   var box_contactclasses = document.getElementById('contactclassesdiv');
252   var box_emailtovoice = document.getElementById('emailtovoicediv');
253
254   box_emailtovoice.style.display = (box_emailtovoice.style.display == 'none') ? 'block' : 'none';
255   document.getElementById('emailtovoice_contact').options[0].selected=true;
256
257   box_contactclasses.style.display = (box_contactclasses.style.display == 'none') ? 'block' : 'none';
258 }
259 </SCRIPT>
260
261 <& /elements/footer.html &>
262
263 <%init>
264
265 my %opt = @_;
266
267 $opt{'acl'} ||= 'Bulk send customer notices';
268
269 my $email_to;
270
271 die "access denied"
272   unless $FS::CurrentUser::CurrentUser->access_right($opt{'acl'});
273
274 my $conf = FS::Conf->new;
275 my @no_search_fields = qw( table from subject html_body text_body popup url );
276
277 my $send_to_domain = $conf->config('email-to-voice_domain');
278
279 my $form_action = $opt{'form_action'} || 'email-customers.html';
280 my $process_url = $opt{'process_url'} || 'process/email-customers.html';
281 my $title = $opt{'title'} || 'Send customer notices';
282 push( @no_search_fields, @{$opt{'no_search_fields'}} ) if $opt{'no_search_fields'};
283
284 $m->comp('/elements/handle_uri_query');
285
286 my $table = $cgi->param('table') or die "'table' required";
287 my $agent_virt_agentnum = $cgi->param('agent_virt_agentnum') || '';
288
289 my $popup = $cgi->param('popup');
290 my $url   = $cgi->param('url');
291 my $pdest = { 'message' => "Notice sent" };
292 $pdest->{'url'} = $cgi->param('url') if $url;
293
294 my %search;
295 if ( $cgi->param('search') ) {
296   %search = %{ thaw(decode_base64( $cgi->param('search') )) };
297 }
298 else {
299   %search = $cgi->Vars;
300   delete $search{$_} for @no_search_fields;
301   # FS::$table->search is expected to know which parameters might be 
302   # multi-valued, and to accept scalar values for them also.  No good 
303   # solution to this since CGI can't tell whether a parameter _might_
304   # have had multiple values, only whether it does.
305   @search{keys %search} = map { /\0/ ? [ split /\0/, $_ ] : $_ } values %search;
306 }
307
308 &{$opt{'post_search_hook'}}(
309   'conf'   => $conf,
310   'search' => \%search,
311 ) if $opt{'post_search_hook'};
312
313 my $num_cust;
314 my $from = '';
315 if ( $cgi->param('from') ) {
316   $from = $cgi->param('from');
317 } elsif ( $cgi->param('from_name') ) {
318   $from = ($cgi->param('from_name') . ' <' . $cgi->param('from_addr') . '>');
319 } elsif ( $cgi->param('from_addr') ) {
320   $from = $cgi->param('from_addr');
321 }
322
323 my $msg_template = '';
324 if ( $cgi->param('msgnum') =~ /^(\d+)$/ ) {
325   $msg_template = FS::msg_template->by_key($1)
326     or die "template not found: ".$cgi->param('msgnum');
327 }
328
329 my @contact_classnum;
330 my @contact_classname;
331
332 my $subject = $cgi->param('subject');
333 my $body = $cgi->param('body');
334 my ($html_body, $text_body);
335
336 if ( !$cgi->param('preview') ) {
337
338   # edit mode: initialize the fields from the saved draft, if there is one
339   if ( $msg_template and $msg_template->disabled eq 'D' ) {
340     my $content = $msg_template->content(''); # no localization on these yet
341     $subject ||= $content->subject;
342     $body ||= $content->body;
343   }
344
345 } else {
346
347   my $sql_query = "FS::$table"->search(\%search);
348   my $count_query = delete($sql_query->{'count_query'});
349   my $count_sth = dbh->prepare($count_query)
350     or die "Error preparing $count_query: ". dbh->errstr;
351   $count_sth->execute
352     or die "Error executing $count_query: ". $count_sth->errstr;
353   my $count_arrayref = $count_sth->fetchrow_arrayref;
354   $num_cust = $count_arrayref->[0];
355
356   if ( !$msg_template or $msg_template->disabled eq 'D' ) {
357     # then this is a one-off template; edit it in place
358     my $subject = $cgi->param('subject') || '';
359     my $body = $cgi->param('body') || '';
360
361     # create a draft template
362     $msg_template ||= FS::msg_template->new({
363       msgclass  => 'email',
364       disabled  => 'D',
365     });
366     # anyone have a better idea for msgname?
367     $msg_template->set('msgname' => "Notice " . DateTime->now->iso8601);
368     $msg_template->set('from_addr' => $from);
369     my %content = (
370       subject => $subject,
371       body    => $body,
372     );
373     my $error;
374     if ( $msg_template->msgnum ) {
375       $error = $msg_template->replace(%content);
376     } else {
377       $error = $msg_template->insert(%content);
378     }
379
380     if ( $error ) {
381       $cgi->param('error', $error);
382       $cgi->delete('preview'); # don't go on to preview stage yet
383       undef $msg_template;
384     }
385   }
386   # unless creating the msg_template failed, we now have one, so construct a
387   # preview message from the first customer/whatever in the search results
388
389   my $cust;
390
391   if ( $msg_template ) { 
392     $sql_query->{'extra_sql'} .= ' LIMIT 1';
393     $sql_query->{'select'} = "$table.*";
394     $sql_query->{'order_by'} = '';
395     my $object = qsearchs($sql_query);
396     $cust = $object->cust_main;
397     my %msgopts = (
398       'cust_main' => $cust,
399       'object' => $object,
400     );
401
402     my $cust_msg = $msg_template->prepare(%msgopts);
403     $from = $cust_msg->env_from;
404     $html_body = $cust_msg->preview;
405 #hmm.  this came in with the #37098 rewrite, but isn't on v3 :/
406 # causing problems with mangling subject of unrelated things
407 # should probably decode instead of ignore the UTF-8 thing, but
408 # this at least masks the ugliness for now :/
409     if ( $cust_msg->header =~ /^subject: (.*)/mi && $1 !~ /^\=\?UTF-8/ ) {
410       $subject = $1;
411     }
412   }
413
414   # contact_class_X params
415   #we can't switch to multi_param until we're done supporting deb 7
416   local($CGI::LIST_CONTEXT_WARN) = 0;
417
418   if ($cgi->param('emailtovoice_contact')) {
419     $email_to = $cgi->param('emailtovoice_contact') . '@' . $send_to_domain;
420     push @contact_classnum, 'emailtovoice';
421     push @contact_classname, $email_to;
422   }
423   else {
424     foreach my $param ( $cgi->param ) {
425       if ( $param =~ /^contact_class_(\w+)$/ ) {
426         push @contact_classnum, $1;
427         if ( $1 eq 'invoice' ) {
428           push @contact_classname, 'Invoice recipients';
429         } elsif ( $1 eq 'message' ) {
430           push @contact_classname, 'Message recipients';
431         } else {
432           my $contact_class = FS::contact_class->by_key($1);
433           push @contact_classname, encode_entities($contact_class->classname);
434         }
435       }
436     }
437   }
438 }
439
440 my @contact_checkboxes = (
441   [ 'message' => { label => 'Message recipients' } ],
442   [ 'invoice' => { label => 'Invoice recipients' } ],
443 );
444
445 foreach my $class (qsearch('contact_class', { disabled => '' })) {
446   push @contact_checkboxes, [
447     $class->classnum,
448     { label => $class->classname }
449   ];
450 }
451
452 </%init>