fix upgrade for dangling cust_event_fee (how?), RT#75113
[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 ) ],
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
73 %   if ( !$msg_template->disabled ) {
74       <& /elements/tr-td-label.html, 'label' => 'Template:' &>
75         <td><% $msg_template->msgname |h %></td>
76       </tr>
77 %   }
78
79       <& /elements/tr-td-label.html, 'label' => 'From:' &>
80         <td><% $from |h %></td>
81       </tr>
82
83       <& /elements/tr-td-label.html, 'label' => 'To contacts:' &>
84         <td><% join('<BR>', @contact_classname) %></td>
85       </tr>
86
87       <& /elements/tr-td-label.html, 'label' => 'Subject:' &>
88         <td><% $subject |h %></td>
89       </tr>
90
91       <TR><TD COLSPAN=2>&nbsp;</TD></TR>
92       <TR>
93         <TH ALIGN="right" VALIGN="top">Message (HTML display): </TD>
94         <TD CLASS="background" ALIGN="left"><% $html_body %></TD>
95       </TR>
96
97 %     my $text_body = HTML::FormatText->new(leftmargin=>0)->format(
98 %                       HTML::TreeBuilder->new_from_content(
99 %                         $html_body
100 %                       )
101 %                     );
102       <TR><TD COLSPAN=2>&nbsp;</TD></TR>
103       <TR>
104         <TH ALIGN="right" VALIGN="top">Message (Text display): </TD>
105         <TD CLASS="background" ALIGN="left">
106           <a href="javascript:void(0)" ID="email-message-text-view" style="color:#666666" onclick="showtext()">(view)</a>
107           <a href="javascript:void(0)" ID="email-message-text-hide" style="color:#666666; display: none;" onclick="hidetext()">(hide)</a>
108           <PRE id="email-message-text" style="display: none;"><% $text_body %></PRE>
109         </TD>
110       </TR>
111
112     </TABLE>
113
114     <SCRIPT>
115
116       function showtext() {
117         $('#email-message-text-view').css('display','none');
118         $('#email-message-text-hide').css('display','');
119         $('#email-message-text').slideDown();
120       }
121
122       function hidetext() {
123         $('#email-message-text-view').css('display','');
124         $('#email-message-text-hide').css('display','none');
125         $('#email-message-text').slideUp();
126       }
127
128       function areyousure(href) {
129         if (confirm("Send this notice to <% ($num_cust > 1) ? "$num_cust customers" : '1 customer' %> ?")) {
130           process();
131         }
132       }
133     </SCRIPT>
134
135     <BR>
136     <INPUT TYPE="submit" NAME="edit" VALUE="Edit">
137     <INPUT TYPE="button" VALUE="Send notice" onClick="areyousure()">
138
139 % } elsif ($opt{'alternate_form'}) {
140
141 <% &{$opt{'alternate_form'}}() %>
142
143 % } else {
144 %   # Edit mode.
145
146 <SCRIPT TYPE="text/javascript">
147 function toggle(obj) {
148   document.getElementById('table_no_template').style.display = (obj.value == 0) ? '' : 'none';
149 }
150
151 </SCRIPT>
152 % if ( $msg_template and $msg_template->disabled ) {
153 %   # if we've already established a draft template, don't let msgnum be changed
154     <& /elements/hidden.html,
155       field => 'msgnum',
156       curr_value => ( scalar($cgi->param('msgnum')) || ''),
157     &>
158 % } else {
159 Template: 
160     <& /elements/select-msg_template.html,
161         onchange   => 'toggle(this)',
162         curr_value => ( scalar($cgi->param('msgnum')) || ''),
163     &>
164     <BR>
165 % }
166 % # select destination contact classes
167 Send to contacts:
168   <& /elements/checkboxes.html,
169     'style'               => 'display: inline; vertical-align: top',
170     'disable_links'       => 1,
171     'names_list'          => \@contact_checkboxes,
172     'element_name_prefix' => 'contact_class_',
173     'checked_callback'    => sub {
174       my($cgi, $name) = @_;
175       $name eq 'invoice' #others default to unchecked
176     },
177   &>
178 <BR>
179 % # if sending a one-off message, show a form to edit it
180   <TABLE BGCOLOR="#cccccc" CELLSPACING=0 WIDTH="100%" id="table_no_template">
181     <& /elements/tr-td-label.html, 'label' => 'From:' &>
182       <TD><& /elements/input-text.html,
183               'field' => 'from_name',
184               'value' => $conf->config('invoice_from_name', $agent_virt_agentnum) ||
185                          $conf->config('company_name', $agent_virt_agentnum), #?
186               'size'  => 20,
187               'curr_value' => scalar($cgi->param('from_name')),
188           &>&nbsp;&lt;\
189           <& /elements/input-text.html,
190               'field' => 'from_addr',
191               'type'  => 'email', # HTML5, woot
192               'value' => $conf->config('invoice_from', $agent_virt_agentnum),
193               'size'  => 20,
194               'curr_value' => scalar($cgi->param('from_addr')),
195           &>&gt;</TD>
196  
197     <& /elements/tr-input-text.html,
198                  'field' => 'subject',
199                  'label' => 'Subject:',
200                  'size'  => 50,
201                  'curr_value' => $subject,
202     &>
203
204     <TR>
205       <TD ALIGN="right" VALIGN="top" STYLE="padding-top:3px">Message: </TD>
206       <TD><& /elements/htmlarea.html, 
207                'field' => 'body',
208                'width' => 763,
209                'curr_value' => $body,
210           &>
211       </TD>
212     </TR>
213
214   </TABLE>
215
216   <INPUT TYPE="submit" NAME="preview" VALUE="Preview notice">
217
218 % } #end not action or alternate form
219
220 </FORM>
221
222 <& /elements/footer.html &>
223
224 <%init>
225
226 my %opt = @_;
227
228 $opt{'acl'} ||= 'Bulk send customer notices';
229
230 die "access denied"
231   unless $FS::CurrentUser::CurrentUser->access_right($opt{'acl'});
232
233 my $conf = FS::Conf->new;
234 my @no_search_fields = qw( table from subject html_body text_body popup url );
235
236 my $form_action = $opt{'form_action'} || 'email-customers.html';
237 my $process_url = $opt{'process_url'} || 'process/email-customers.html';
238 my $title = $opt{'title'} || 'Send customer notices';
239 push( @no_search_fields, @{$opt{'no_search_fields'}} ) if $opt{'no_search_fields'};
240
241 my $table = $cgi->param('table') or die "'table' required";
242 my $agent_virt_agentnum = $cgi->param('agent_virt_agentnum') || '';
243
244 my $popup = $cgi->param('popup');
245 my $url   = $cgi->param('url');
246 my $pdest = { 'message' => "Notice sent" };
247 $pdest->{'url'} = $cgi->param('url') if $url;
248
249 my %search;
250 if ( $cgi->param('search') ) {
251   %search = %{ thaw(decode_base64( $cgi->param('search') )) };
252 }
253 else {
254   %search = $cgi->Vars;
255   delete $search{$_} for @no_search_fields;
256   # FS::$table->search is expected to know which parameters might be 
257   # multi-valued, and to accept scalar values for them also.  No good 
258   # solution to this since CGI can't tell whether a parameter _might_
259   # have had multiple values, only whether it does.
260   @search{keys %search} = map { /\0/ ? [ split /\0/, $_ ] : $_ } values %search;
261 }
262
263 &{$opt{'post_search_hook'}}(
264   'conf'   => $conf,
265   'search' => \%search,
266 ) if $opt{'post_search_hook'};
267
268 my $num_cust;
269 my $from = '';
270 if ( $cgi->param('from') ) {
271   $from = $cgi->param('from');
272 } elsif ( $cgi->param('from_name') ) {
273   $from = ($cgi->param('from_name') . ' <' . $cgi->param('from_addr') . '>');
274 } elsif ( $cgi->param('from_addr') ) {
275   $from = $cgi->param('from_addr');
276 }
277
278 my $msg_template = '';
279 if ( $cgi->param('msgnum') =~ /^(\d+)$/ ) {
280   $msg_template = FS::msg_template->by_key($1)
281     or die "template not found: ".$cgi->param('msgnum');
282 }
283
284 my @contact_classnum;
285 my @contact_classname;
286
287 my $subject = $cgi->param('subject');
288 my $body = $cgi->param('body');
289 my ($html_body, $text_body);
290
291 if ( !$cgi->param('preview') ) {
292
293   # edit mode: initialize the fields from the saved draft, if there is one
294   if ( $msg_template and $msg_template->disabled eq 'D' ) {
295     my $content = $msg_template->content(''); # no localization on these yet
296     $subject ||= $content->subject;
297     $body ||= $content->body;
298   }
299
300 } else {
301
302   my $sql_query = "FS::$table"->search(\%search);
303   my $count_query = delete($sql_query->{'count_query'});
304   my $count_sth = dbh->prepare($count_query)
305     or die "Error preparing $count_query: ". dbh->errstr;
306   $count_sth->execute
307     or die "Error executing $count_query: ". $count_sth->errstr;
308   my $count_arrayref = $count_sth->fetchrow_arrayref;
309   $num_cust = $count_arrayref->[0];
310
311   if ( !$msg_template or $msg_template->disabled eq 'D' ) {
312     # then this is a one-off template; edit it in place
313     my $subject = $cgi->param('subject') || '';
314     my $body = $cgi->param('body') || '';
315
316     # create a draft template
317     $msg_template ||= FS::msg_template->new({
318       msgclass  => 'email',
319       disabled  => 'D',
320     });
321     # anyone have a better idea for msgname?
322     $msg_template->set('msgname' => "Notice " . DateTime->now->iso8601);
323     $msg_template->set('from_addr' => $from);
324     my %content = (
325       subject => $subject,
326       body    => $body,
327     );
328     my $error;
329     if ( $msg_template->msgnum ) {
330       $error = $msg_template->replace(%content);
331     } else {
332       $error = $msg_template->insert(%content);
333     }
334
335     if ( $error ) {
336       $cgi->param('error', $error);
337       $cgi->delete('preview'); # don't go on to preview stage yet
338       undef $msg_template;
339     }
340   }
341   # unless creating the msg_template failed, we now have one, so construct a
342   # preview message from the first customer/whatever in the search results
343
344   if ( $msg_template ) { 
345     $sql_query->{'extra_sql'} .= ' LIMIT 1';
346     $sql_query->{'select'} = "$table.*";
347     $sql_query->{'order_by'} = '';
348     my $object = qsearchs($sql_query);
349     my $cust = $object->cust_main;
350     my %msgopts = (
351       'cust_main' => $cust,
352       'object' => $object,
353     );
354
355     my $cust_msg = $msg_template->prepare(%msgopts);
356     $from = $cust_msg->env_from;
357     $html_body = $cust_msg->preview;
358 #hmm.  this came in with the #37098 rewrite, but isn't on v3 :/
359 # causing problems with mangling subject of unrelated things
360 # should probably decode instead of ignore the UTF-8 thing, but
361 # this at least masks the ugliness for now :/
362     if ( $cust_msg->header =~ /^subject: (.*)/mi && $1 !~ /^\=\?UTF-8/ ) {
363       $subject = $1;
364     }
365   }
366
367   # contact_class_X params
368   #we can't switch to multi_param until we're done supporting deb 7
369   local($CGI::LIST_CONTEXT_WARN) = 0;
370   foreach my $param ( $cgi->param ) {
371     if ( $param =~ /^contact_class_(\w+)$/ ) {
372       push @contact_classnum, $1;
373       if ( $1 eq 'invoice' ) {
374         push @contact_classname, 'Invoice recipients';
375       } else {
376         my $contact_class = FS::contact_class->by_key($1);
377         push @contact_classname, encode_entities($contact_class->classname);
378       }
379     }
380   }
381 }
382
383 my @contact_checkboxes = (
384   [ 'invoice' => { label => 'Invoice recipients' } ]
385 );
386 foreach my $class (qsearch('contact_class', { disabled => '' })) {
387   push @contact_checkboxes, [
388     $class->classnum,
389     { label => $class->classname }
390   ];
391 }
392 </%init>