fix broadband reporting with giant query URLs (large numbers of package defs, etc...
[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 $m->comp('/elements/handle_uri_query');
242
243 my $table = $cgi->param('table') or die "'table' required";
244 my $agent_virt_agentnum = $cgi->param('agent_virt_agentnum') || '';
245
246 my $popup = $cgi->param('popup');
247 my $url   = $cgi->param('url');
248 my $pdest = { 'message' => "Notice sent" };
249 $pdest->{'url'} = $cgi->param('url') if $url;
250
251 my %search;
252 if ( $cgi->param('search') ) {
253   %search = %{ thaw(decode_base64( $cgi->param('search') )) };
254 }
255 else {
256   %search = $cgi->Vars;
257   delete $search{$_} for @no_search_fields;
258   # FS::$table->search is expected to know which parameters might be 
259   # multi-valued, and to accept scalar values for them also.  No good 
260   # solution to this since CGI can't tell whether a parameter _might_
261   # have had multiple values, only whether it does.
262   @search{keys %search} = map { /\0/ ? [ split /\0/, $_ ] : $_ } values %search;
263 }
264
265 &{$opt{'post_search_hook'}}(
266   'conf'   => $conf,
267   'search' => \%search,
268 ) if $opt{'post_search_hook'};
269
270 my $num_cust;
271 my $from = '';
272 if ( $cgi->param('from') ) {
273   $from = $cgi->param('from');
274 } elsif ( $cgi->param('from_name') ) {
275   $from = ($cgi->param('from_name') . ' <' . $cgi->param('from_addr') . '>');
276 } elsif ( $cgi->param('from_addr') ) {
277   $from = $cgi->param('from_addr');
278 }
279
280 my $msg_template = '';
281 if ( $cgi->param('msgnum') =~ /^(\d+)$/ ) {
282   $msg_template = FS::msg_template->by_key($1)
283     or die "template not found: ".$cgi->param('msgnum');
284 }
285
286 my @contact_classnum;
287 my @contact_classname;
288
289 my $subject = $cgi->param('subject');
290 my $body = $cgi->param('body');
291 my ($html_body, $text_body);
292
293 if ( !$cgi->param('preview') ) {
294
295   # edit mode: initialize the fields from the saved draft, if there is one
296   if ( $msg_template and $msg_template->disabled eq 'D' ) {
297     my $content = $msg_template->content(''); # no localization on these yet
298     $subject ||= $content->subject;
299     $body ||= $content->body;
300   }
301
302 } else {
303
304   my $sql_query = "FS::$table"->search(\%search);
305   my $count_query = delete($sql_query->{'count_query'});
306   my $count_sth = dbh->prepare($count_query)
307     or die "Error preparing $count_query: ". dbh->errstr;
308   $count_sth->execute
309     or die "Error executing $count_query: ". $count_sth->errstr;
310   my $count_arrayref = $count_sth->fetchrow_arrayref;
311   $num_cust = $count_arrayref->[0];
312
313   if ( !$msg_template or $msg_template->disabled eq 'D' ) {
314     # then this is a one-off template; edit it in place
315     my $subject = $cgi->param('subject') || '';
316     my $body = $cgi->param('body') || '';
317
318     # create a draft template
319     $msg_template ||= FS::msg_template->new({
320       msgclass  => 'email',
321       disabled  => 'D',
322     });
323     # anyone have a better idea for msgname?
324     $msg_template->set('msgname' => "Notice " . DateTime->now->iso8601);
325     $msg_template->set('from_addr' => $from);
326     my %content = (
327       subject => $subject,
328       body    => $body,
329     );
330     my $error;
331     if ( $msg_template->msgnum ) {
332       $error = $msg_template->replace(%content);
333     } else {
334       $error = $msg_template->insert(%content);
335     }
336
337     if ( $error ) {
338       $cgi->param('error', $error);
339       $cgi->delete('preview'); # don't go on to preview stage yet
340       undef $msg_template;
341     }
342   }
343   # unless creating the msg_template failed, we now have one, so construct a
344   # preview message from the first customer/whatever in the search results
345
346   if ( $msg_template ) { 
347     $sql_query->{'extra_sql'} .= ' LIMIT 1';
348     $sql_query->{'select'} = "$table.*";
349     $sql_query->{'order_by'} = '';
350     my $object = qsearchs($sql_query);
351     my $cust = $object->cust_main;
352     my %msgopts = (
353       'cust_main' => $cust,
354       'object' => $object,
355     );
356
357     my $cust_msg = $msg_template->prepare(%msgopts);
358     $from = $cust_msg->env_from;
359     $html_body = $cust_msg->preview;
360 #hmm.  this came in with the #37098 rewrite, but isn't on v3 :/
361 # causing problems with mangling subject of unrelated things
362 # should probably decode instead of ignore the UTF-8 thing, but
363 # this at least masks the ugliness for now :/
364     if ( $cust_msg->header =~ /^subject: (.*)/mi && $1 !~ /^\=\?UTF-8/ ) {
365       $subject = $1;
366     }
367   }
368
369   # contact_class_X params
370   #we can't switch to multi_param until we're done supporting deb 7
371   local($CGI::LIST_CONTEXT_WARN) = 0;
372   foreach my $param ( $cgi->param ) {
373     if ( $param =~ /^contact_class_(\w+)$/ ) {
374       push @contact_classnum, $1;
375       if ( $1 eq 'invoice' ) {
376         push @contact_classname, 'Invoice recipients';
377       } else {
378         my $contact_class = FS::contact_class->by_key($1);
379         push @contact_classname, encode_entities($contact_class->classname);
380       }
381     }
382   }
383 }
384
385 my @contact_checkboxes = (
386   [ 'invoice' => { label => 'Invoice recipients' } ]
387 );
388 foreach my $class (qsearch('contact_class', { disabled => '' })) {
389   push @contact_checkboxes, [
390     $class->classnum,
391     { label => $class->classname }
392   ];
393 }
394 </%init>