address standardization, part 2
[freeside.git] / httemplate / elements / location.html
1 <%doc>
2
3 Example:
4
5   include( '/elements/location.html',
6              'object'         => $cust_location
7              'prefix'         => $pre, # prefixed to form field names
8              'onchange'       => $javascript,
9              'geocode'        => $geocode, #passed through
10              'censustract'    => $censustract, #passed through
11              'no_asterisks'   => 0, #set true to disable the red asterisks next
12                                     #to required fields
13              'address1_label' => 'Address', #label for address
14              'enable_coords'  => 1, #show latitude/longitude fields
15              'enable_district' => 1, #show tax district field
16              'enable_censustract' => 1, #show censustract field
17              
18          )
19
20 </%doc>
21
22 <SCRIPT SRC="<% $fsurl %>elements/jquery.deserialize.min.js"></SCRIPT>
23 <SCRIPT SRC="<% $fsurl %>elements/polyfill.js"></SCRIPT>
24
25 % if ( $opt{'alt_format'} ) {
26
27 <TR>
28     <<%$th%> ALIGN="right">Location&nbsp;kind</<%$th%>>
29     <TD>
30     <% include('/elements/select.html',
31                  'cgi'        => $cgi,
32                  'field'      => 'location_kind',
33                  'id'         => 'location_kind',
34                  'disabled'   => $disabled,
35                  #'style'      => \@style,
36                  'options'    => \@location_kind_options,
37                  'labels'     => $location_kind_labels,
38                  'curr_value' => scalar($cgi->param('location_kind'))
39                                    || $object->get('location_kind'),
40               )
41     %>
42     </TD>
43   </TR>
44
45 % } 
46
47 % if ( $label_prefix eq '_location' ) {
48
49     <TR>
50       <TH ALIGN="right" ><% $opt{'locationname_label'} || emt('Location ID') %></TD>
51       <TD COLSPAN=7>
52         <INPUT TYPE     = "text"
53                NAME     = "<%$pre%>locationname"
54                ID       = "<%$pre%>locationname"
55                VALUE    = "<% $object->get('locationname') |h %>"
56                SIZE     = 24
57                onChange = "<% $onchange %>"
58                <% $disabled %>
59                <% $style %>
60         >
61       </TD>
62     </TR>
63
64 % } else {
65     <& hidden.html, field => $pre.'locationname', value => $object->get('locationname') &>
66
67 % }
68
69 <TR>
70   <<%$th%> STYLE="width:16ex" ALIGN="right"><%$r%><% $opt{'address1_label'} || emt('Address') %></<%$th%>>
71   <TD COLSPAN=7>
72     <INPUT TYPE     = "text"
73            NAME     = "<%$pre%>address1"
74            ID       = "<%$pre%>address1"
75            VALUE    = "<% $object->get('address1') |h %>"
76            SIZE     = 54
77            onChange = "<% $onchange %>"
78            <% $disabled %>
79            <% $style %>
80     >
81   </TD>
82 </TR>
83
84 % if ( ! $opt{'alt_format'} ) { #regular format
85
86 <TR>
87       <TH ALIGN="right"><FONT ID="<% $pre %>address2_required" color="#ff0000" <% $address2_label_style %>>*</FONT>&nbsp;<FONT ID="<% $pre %>address2_label" <% $address2_label_style %>><B>Unit&nbsp;#</B></FONT></TD>
88       <TD COLSPAN=7>
89         <INPUT TYPE     = "text"
90                NAME     = "<%$pre%>address2"
91                ID       = "<%$pre%>address2"
92                VALUE    = "<% $object->get('address2') |h %>"
93                SIZE     = 54
94                onChange = "<% $onchange %>"
95                <% $disabled %>
96                <% $style %>
97         >
98       </TD>
99 </TR>
100
101 % } else { # alternate format
102
103 <& hidden.html, field => $pre.'address2', value => $object->get('address2') &>
104
105 <TR>
106     <<%$th%> ALIGN="right">Unit&nbsp;type&nbsp;and&nbsp;#</<%$th%>>
107     <TD COLSPAN=7>
108
109 %     my $location_type = scalar($cgi->param('location_type'))
110 %                           || $object->get('location_type');
111 %     #my $location_number = scalar($cgi->param('location_number'))
112 %     #                        || $object->get($pre.'location_number');
113 %
114 %   if ( $object->get($pre.'address2') && ! $location_type ) {
115 %   }
116 %
117 %     if ( 1 ) { #ikano, switch on via config
118 %       tie my %location_types, 'Tie::IxHash',
119 %         FS::part_export::ikano->location_types;
120         <% include('/elements/select.html',
121                      'cgi'        => $cgi,
122                      'field'      => 'location_type',
123                      'id'         => 'location_type',
124                      'disabled'   => $disabled,
125                      #'style'      => \@style,
126                      'options'    => [ keys %location_types ],
127                      'labels'     => \%location_types,
128                      'curr_value' => $location_type,
129                      'onchange'   => 'location_type_changed',
130                   )
131         %>
132         <SCRIPT TYPE="text/javascript">
133           function location_type_changed (what) {
134             if ( what.options[what.selectedIndex].value == '' ) {
135               what.form.location_number.disabled = true;
136               what.form.location_number.style.backgroundColor = '#dddddd';
137             } else {
138               what.form.location_number.disabled = false;
139               what.form.location_number.style.backgroundColor = '#ffffff';
140             }
141           }
142         </SCRIPT>
143 %     } else {
144         <INPUT TYPE  = "text" 
145                NAME  = "location_type" 
146                ID    = "location_type"
147                VALUE = "<% $location_type |h %>"
148                SIZE  = "10"
149                <% $disabled %>
150                <% $style %>
151         >
152 %     }
153
154     <INPUT TYPE="text" 
155                NAME  = "location_number"
156                ID    = "location_number"
157                VALUE = "<% scalar($cgi->param('location_number')) || $object->get('location_number') |h %>"
158                SIZE  = "5"
159                <% $disabled || ($location_type ? '' : 'DISABLED') %>
160                <% $style %>
161         >
162
163 %    #XXX i don't work so well when the dropdown is changed :/  i probably need to be triggered by "default service address"
164 %    $alt_err =~ s/(ship_)?address2/'<B>'.encode_entities($object->get($1.'address2')).'<\/B>'/e;
165      <% $alt_err %>
166
167     </TD>
168
169 </TR>
170
171 % } 
172
173
174 <TR>
175   <<%$th%> ALIGN="right">
176 % unless ($conf->exists('cust_main-no_city_in_address')) {
177   <% $r %><% mt('City') |h %>
178 % }
179   </<%$th%>>
180   <TD WIDTH="1"><% include('/elements/city.html', %select_hash, 'text_style' => \@style ) %></TD>
181   <<%$th%> ALIGN="right" WIDTH="1" ID="<%$pre%>countylabel" <%$county_style%>><%$r%>County</<%$th%>>
182   <TD WIDTH="1"><% include('/elements/select-county.html', %select_hash ) %></TD>
183   <<%$th%> ALIGN="right" WIDTH="1"><%$r%><% mt('State') |h %></<%$th%>>
184   <TD WIDTH="1">
185     <% include('/elements/select-state.html', %select_hash ) %>
186   </TD>
187   <<%$th%> ALIGN="right" WIDTH="1"><%$r%><% mt('Zip') |h %></<%$th%>>
188   <TD>
189     <INPUT TYPE     = "text"
190            NAME     = "<%$pre%>zip"
191            ID       = "<%$pre%>zip"
192            VALUE    = "<% $object->get('zip') |h %>"
193            SIZE     = 11
194            onChange = "<% $onchange %>"
195            <% $disabled %>
196            <% $style %>
197     >
198   </TD>
199 </TR>
200
201 <TR>
202   <<%$th%> ALIGN="right"><%$r%><% mt('Country') |h %></<%$th%>>
203   <TD COLSPAN=6><% include('/elements/select-country.html', %select_hash ) %></TD>
204 </TR>
205
206 % if ( $opt{enable_coords} ) {
207 <TR>
208   <TH ALIGN="right"><% mt('Latitude') |h %></TD>
209   <TD COLSPAN=7>
210     <INPUT TYPE  = "text"
211            NAME  = "<%$pre%>latitude"
212            ID    = "<%$pre%>latitude"
213            VALUE = "<% $object->get('latitude') |h %>"
214            <% $disabled %>
215            <% $style %>
216     >
217     <FONT SIZE="-1" COLOR="#666666"><% mt('Longitude') |h %></FONT>
218     <INPUT TYPE  = "text"
219            NAME  = "<%$pre%>longitude"
220            ID    = "<%$pre%>longitude"
221            VALUE = "<% $object->get('longitude') |h %>"
222            <% $disabled %>
223            <% $style %>
224     >
225   </TD>
226 </TR>
227 % } else {
228 %   foreach (qw(latitude longitude)) {
229 <& hidden.html, field => $pre.$_, value => $object->get($_) &>
230 %   }
231 % }
232 %
233 % foreach (qw(coord_auto geocode censustract censusyear)) {
234   <& hidden.html, field => $pre.$_, value => $object->get($_) &>
235 % }
236 %
237 % if ( $opt{enable_censustract} ) {
238 <TR>
239   <TH ALIGN="right">Census&nbsp;tract</TD>
240   <TD COLSPAN=8>
241     <INPUT TYPE="text" SIZE=15
242            ID="<% $pre %>enter_censustract" 
243            NAME="<% $pre %>enter_censustract" 
244            VALUE="<% $object->censustract |h %>">
245     <FONT SIZE="-1" COLOR="#333333"><% '(automatic)' %></FONT>
246   </TD>
247 </TR>
248 % }
249 % if ( $opt{enable_district} and $conf->config('tax_district_method') ) {
250   <TR>
251     <TD ALIGN="right">Tax&nbsp;district</TD>
252     <TD COLSPAN=8>
253       <INPUT TYPE="text" SIZE=15
254              NAME="<%$pre%>district" 
255              ID="<%$pre%>district"
256              VALUE="<% $object->district |h %>">
257     <% '(automatic)' %>
258     </TD>
259   </TR>
260 % } else {
261   <& hidden.html, field => $pre.'district', value => $object->get('district') &>
262 % }
263
264 %# For address standardization:
265 %# keep a clean copy of the address so we know if we need
266 %# to re-standardize
267 % foreach (qw(locationname address1 city state country zip latitude
268 %             longitude censustract district addr_clean
269 %             ) ) {
270 <& hidden.html, field => 'old_'.$pre.$_, value => $object->get($_) &>
271 % }
272 %# Placeholders
273 <& hidden.html, field => $pre.'cachenum', value => '' &>
274 <& hidden.html, field => $pre.'addr_clean', value => $object->get('addr_clean') &>
275
276 <SCRIPT TYPE="text/javascript">
277 // XXX some of this should go away after address standardization changes...
278 <&| /elements/onload.js &>
279   var clear_coords_ids = [
280     '<%$pre%>latitude',
281     '<%$pre%>longitude',
282     '<%$pre%>enter_censustract',
283     '<%$pre%>censustract',
284     '<%$pre%>district'
285   ];
286   function clear_coords() {
287     for (var i=0; i < clear_coords_ids.length; i++) {
288       var el = document.getElementById(clear_coords_ids[i]);
289       if ( el ) {
290         el.value = '';
291       }
292     }
293   }
294   var clear_coords_on_change = [
295     '<%$pre%>address1',
296     '<%$pre%>address2',
297     <% $conf->exists('cust_main-no_city_in_address') ? '' : qq('${pre}city',) %>
298     '<%$pre%>state',
299     '<%$pre%>zip',
300     '<%$pre%>country'
301   ];
302   for (var i=0; i < clear_coords_on_change.length; i++) {
303     var el = document.getElementById(clear_coords_on_change[i]);
304     if ( el.addEventListener ) {
305       el.addEventListener('change', clear_coords);
306     } else if ( el.attachEvent ) {
307       el.attachEvent('onchange', clear_coords);
308     }
309   }
310   function clear_censustract() {
311     // if the user manually edits the census tract, clear the 'hard' census
312     // tract field so that we can re-verify and present a confirmation popup 
313
314     // get the ID of the hidden censustract field
315     var censustract_id = this.id.replace('enter_', '');
316     var el = document.getElementById(censustract_id);
317     if (el) {
318       el.value = '';
319     }
320   }
321   var el = document.getElementById('<%$pre%>enter_censustract');
322   if (el) {
323     if ( el.addEventListener ) {
324       el.addEventListener('change', clear_censustract);
325     } else if ( el.attachEvent ) {
326       el.attachEvent('onchange', clear_censustract);
327     }
328   }
329
330 </&>
331
332 % if (! $m->notes('location_js') ) {
333 %   $m->notes('location_js', 1);
334
335 function Location(fieldset, prefix) {
336   if ( typeof fieldset == 'String' ) {
337     fieldset = $('#' + fieldset);
338   }
339   this.fieldset = $(fieldset);
340   this.prefix = prefix || '';
341
342   var errorbox = document.createElement('DIV');
343   errorbox.className = 'error';
344   fieldset.append(errorbox); // after the <table>
345   $(errorbox).position({
346     my: 'left',
347     at: 'right+20px',
348     of: fieldset
349   });
350   this.errorbox = $(errorbox); // so we can find it
351
352   var img_tick = $('<IMG SRC="<% $fsurl %>images/tick.png">');
353   var img_wait = $('<IMG SRC="<% $fsurl %>images/wait-orange.gif">');
354
355   if ( $('#' + prefix + 'addr_clean').prop('value') ) {
356     $(errorbox).append(img_tick);
357   }
358
359   // get/set the serialized (URL parameter string) contents of the form fields
360   this.value = function(newvalue) {
361     if (newvalue) {
362       try {
363         this.fieldset.deserialize(newvalue);
364         this.errorbox.empty();
365         if ( newvalue['error'] ) {
366           this.errorbox.text(newvalue['error']);
367         } else {
368           this.errorbox.append(img_tick);
369         }
370       } catch(err) {
371         console.log("Couldn't parse returned data:\n" + newvalue);
372         // show an error also
373       }
374     }
375     return this.fieldset.serialize();
376   };
377
378   // send a standardization request and push the result into this.value()
379   this.standardize = function(callback) {
380     this.errorbox.empty();
381     this.errorbox.append(img_wait);
382     $.ajax({
383       type: 'POST',
384       url: '<% $fsurl %>misc/address_standardize.cgi',
385       success: this.value.bind(this),
386       data: this.value()
387     });
388   };
389
390   var location_change_timer;
391
392   // check if required fields are filled, and if so, wait 2 seconds, then
393   // call "standardize"
394   var standardize_if_ready = function( ev ) {
395     // pull the location object back out
396     var loc = ev.data;
397
398     if ( location_change_timer ) {
399       console.log("reset timer...");
400       window.clearTimeout(location_change_timer);
401     }
402
403     // require address1 and either (zip) or (city and state)
404     // before trying to standardize
405     var ready =
406          $('#' + loc.prefix + 'address1').val()
407       && (
408            (    $('#' + loc.prefix + 'city').val()
409              && $('#' + loc.prefix + 'state').val()
410            )
411            || $('#' + loc.prefix + 'zip').val()
412          )
413     ;
414
415     if ( ready ) {
416       console.log("start timer...");
417       location_change_timer = window.setTimeout( loc.standardize.bind(loc),
418                                                  2000 );
419     }
420   };
421
422   // event handler; the Location object is passed in event.data
423   var location_changed = standardize_if_ready;
424
425   // bind only to the fields that are used in standardization
426   // (so that it's possible to manually edit coords, etc.)
427   var onchange_fields = 
428     [ 'address1', 'address2', 'city', 'state', 'zip', 'country' ];
429   for ( var i = 0; i < onchange_fields.length; i++ ) {
430     $('#' + prefix + onchange_fields[i]).on('change', this, location_changed);
431   }
432 }
433
434 % }
435 </SCRIPT>
436
437 <%init>
438
439 my %opt = @_;
440
441 my $pre      = $opt{'prefix'};
442 my $object   = $opt{'object'};
443 my $onchange = $opt{'onchange'};
444 my $disabled = $opt{'disabled'};
445
446 my $r = $opt{'no_asterisks'} ? '' : qq!<font color="#ff0000">*</font>&nbsp;!;
447
448 my $conf = new FS::Conf;
449 my $countrydefault = $conf->config('countrydefault') || 'US';
450 my $statedefault   = $conf->config('statedefault') 
451                        || ($countrydefault eq 'US' ? 'CA' : '');
452 my $label_prefix   = $conf->config('cust_location-label_prefix');
453
454 $object ||= FS::cust_location->new({
455   'country' => $countrydefault,
456   'state'   => $statedefault,
457 });
458
459 my $alt_err = ($opt{'alt_format'} && !$disabled) ? $object->alternize : '';
460
461 my @style = ();
462 push @style, 'background-color: #dddddd' if $disabled;
463
464 my @address2_label_style = ();
465 push @address2_label_style, 'visibility:hidden'
466   if $disabled
467   || ! $conf->exists('cust_main-require_address2')
468   || ( !$pre && !$opt{'same_checked'} );
469
470 my @counties = counties( $object->get('state'),
471                          $object->get('country'),
472                        );
473 my @county_style = ();
474 push @county_style, 'display:none' # 'visibility:hidden'
475   unless scalar(@counties) > 1;
476
477 my $style =
478   scalar(@style)
479     ? 'STYLE="'. join(';', @style). '"'
480     : '';
481 my $address2_label_style =
482   scalar(@address2_label_style)
483     ? 'STYLE="'. join(';', @address2_label_style). '"'
484     : '';
485 my $county_style = 
486   scalar(@county_style)
487     ? 'STYLE="'. join(';', @county_style). '"'
488     : '';
489
490 my %select_hash = (
491   'city'     => $object->get('city'),
492   'county'   => $object->get('county'),
493   'state'    => $object->get('state'),
494   'country'  => $object->get('country'),
495   'prefix'   => $pre,
496   'onchange' => $onchange,
497   'disabled' => $disabled,
498   #'style'    => \@style,
499 );
500
501 my $th = $opt{'no_bold'} ? 'TD' : 'TH';
502
503 my @location_kind_options = ( '', 'R', 'B' );
504 my $location_kind_labels = { '' => '', 'R' => 'Residential', 'B' => 'Business' };
505
506 </%init>