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