summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Wells <mark@freeside.biz>2015-10-29 13:49:45 -0700
committerMark Wells <mark@freeside.biz>2015-10-29 13:49:45 -0700
commitb2787f77f842a16df069227e74a2da54b8b36efb (patch)
tree5d857b719ed8209fc413cf65e673e2118f3feb81
parent07b346c23e24d7fb368935b964c091421517dbc0 (diff)
address standardization UI, part 1
-rw-r--r--FS/FS/Misc/Geo.pm2
-rw-r--r--httemplate/docs/license.html2
-rwxr-xr-xhttemplate/edit/cust_main.cgi16
-rw-r--r--httemplate/elements/freeside.css8
-rw-r--r--httemplate/elements/jquery.deserialize.min.js8
-rw-r--r--httemplate/elements/location.html86
-rw-r--r--httemplate/elements/polyfill.js30
-rw-r--r--httemplate/misc/address_standardize.cgi51
8 files changed, 197 insertions, 6 deletions
diff --git a/FS/FS/Misc/Geo.pm b/FS/FS/Misc/Geo.pm
index 1aa593974..293748c7a 100644
--- a/FS/FS/Misc/Geo.pm
+++ b/FS/FS/Misc/Geo.pm
@@ -342,7 +342,7 @@ sub standardize_uscensus {
die "Geocoding did not find a matching address.\n";
} else {
warn Dumper($result) if $DEBUG;
- die $result->error_message;
+ die $result->error_message."\n";
}
}
diff --git a/httemplate/docs/license.html b/httemplate/docs/license.html
index 7e5bb1e3e..71643fcb3 100644
--- a/httemplate/docs/license.html
+++ b/httemplate/docs/license.html
@@ -130,6 +130,8 @@ and other contributors, licensed under the terms of the MIT license.
Contains the Spectrum No Hassle jQuery Colorpicker by Brian Grinstead, licensed
under the terms of the MIT license.
+<P>
+Contains <a href="https://github.com/kflorence/jquery-deserialize/">jQuery.deserialize</a> by Kyle Florence, licensed under the terms of the MIT license.
<!-- artwork -->
diff --git a/httemplate/edit/cust_main.cgi b/httemplate/edit/cust_main.cgi
index effe84b96..25932019e 100755
--- a/httemplate/edit/cust_main.cgi
+++ b/httemplate/edit/cust_main.cgi
@@ -43,6 +43,7 @@
<TD>
%#; padding-right:2px; vertical-align:top">
<FONT CLASS="fsinnerbox-title"><% mt('Billing address') |h %></FONT>
+ <FIELDSET ID="bill_location" CLASS="location">
<TABLE CLASS="fsinnerbox" WIDTH="100%">
<& cust_main/before_bill_location.html, $cust_main &>
<& /elements/location.html,
@@ -54,6 +55,7 @@
&>
<& cust_main/after_bill_location.html, $cust_main &>
</TABLE>
+ </FIELDSET>
</TD>
</TR>
<TR><TD STYLE="height:14px"></TD></TR>
@@ -68,7 +70,7 @@
VALUE="Y"
<% $has_ship_address ? '' : 'CHECKED' %>
><% mt('same as billing address') |h %>
- <DIV ID="div_ship_location">
+ <FIELDSET ID="ship_location" CLASS="location">
<TABLE WIDTH="100%" CLASS="fsinnerbox">
<& cust_main/before_ship_location.html, $cust_main &>
<& /elements/location.html,
@@ -91,7 +93,7 @@
</TR>
% }
</TABLE>
- </DIV>
+ </FIELDSET>
</TD>
</TR></TABLE>
@@ -99,16 +101,20 @@
function samechanged(what) {
if ( what.checked ) {
- $('#div_ship_location').slideUp();
+ $('#ship_location').slideUp();
} else {
- $('#div_ship_location').slideDown();
+ $('#ship_location').slideDown();
}
}
% if ( ! $has_ship_address ) {
- $('#div_ship_location').hide();
+ $('#ship_location').hide();
% }
+$().ready( function() {
+ window.bill_location = new Location($('fieldset#bill_location'));
+});
+
</SCRIPT>
<& cust_main/contacts_new.html, 'cust_main'=>$cust_main, &>
diff --git a/httemplate/elements/freeside.css b/httemplate/elements/freeside.css
index dbd27cbaa..5eb8f720d 100644
--- a/httemplate/elements/freeside.css
+++ b/httemplate/elements/freeside.css
@@ -335,3 +335,11 @@ div.package-marker-change_from {
border-left: solid #bbffbb 30px;
display: inline-block;
}
+
+/* elements/location.html and co. */
+fieldset.location {
+ padding: 0px;
+ margin: 0px;
+ border: none;
+}
+
diff --git a/httemplate/elements/jquery.deserialize.min.js b/httemplate/elements/jquery.deserialize.min.js
new file mode 100644
index 000000000..7054ea4bb
--- /dev/null
+++ b/httemplate/elements/jquery.deserialize.min.js
@@ -0,0 +1,8 @@
+/**
+ * @author Kyle Florence <kyle[dot]florence[at]gmail[dot]com>
+ * @website https://github.com/kflorence/jquery-deserialize/
+ * @version 1.2.1
+ *
+ * Dual licensed under the MIT and GPLv2 licenses.
+ */
+(function(i,b){var f=Array.prototype.push,a=/^(?:radio|checkbox)$/i,e=/\+/g,d=/^(?:option|select-one|select-multiple)$/i,g=/^(?:button|color|date|datetime|datetime-local|email|hidden|month|number|password|range|reset|search|submit|tel|text|textarea|time|url|week)$/i;function c(j){return j.map(function(){return this.elements?i.makeArray(this.elements):this}).filter(":input").get()}function h(j){var k,l={};i.each(j,function(n,m){k=l[m.name];l[m.name]=k===b?m:(i.isArray(k)?k.concat(m):[k,m])});return l}i.fn.deserialize=function(A,l){var y,n,q=c(this),t=[];if(!A||!q.length){return this}if(i.isArray(A)){t=A}else{if(i.isPlainObject(A)){var B,w;for(B in A){i.isArray(w=A[B])?f.apply(t,i.map(w,function(j){return{name:B,value:j}})):f.call(t,{name:B,value:w})}}else{if(typeof A==="string"){var v;A=A.split("&");for(y=0,n=A.length;y<n;y++){v=A[y].split("=");f.call(t,{name:decodeURIComponent(v[0]),value:decodeURIComponent(v[1].replace(e,"%20"))})}}}}if(!(n=t.length)){return this}var u,k,x,z,C,o,m,w,p=i.noop,s=i.noop,r={};l=l||{};q=h(q);if(i.isFunction(l)){s=l}else{p=i.isFunction(l.change)?l.change:p;s=i.isFunction(l.complete)?l.complete:s}for(y=0;y<n;y++){u=t[y];C=u.name;w=u.value;if(!(k=q[C])){continue}m=(z=k.length)?k[0]:k;m=(m.type||m.nodeName).toLowerCase();o=null;if(g.test(m)){if(z){x=r[C];k=k[r[C]=(x==b)?0:++x]}p.call(k,(k.value=w))}else{if(a.test(m)){o="checked"}else{if(d.test(m)){o="selected"}}}if(o){if(!z){k=[k];z=1}for(x=0;x<z;x++){u=k[x];if(u.value==w){p.call(u,(u[o]=true)&&w)}}}}s.call(this);return this}})(jQuery);
diff --git a/httemplate/elements/location.html b/httemplate/elements/location.html
index b5f0a963c..d4c0f14d4 100644
--- a/httemplate/elements/location.html
+++ b/httemplate/elements/location.html
@@ -19,6 +19,9 @@ Example:
</%doc>
+<SCRIPT SRC="<% $fsurl %>elements/jquery.deserialize.min.js"></SCRIPT>
+<SCRIPT SRC="<% $fsurl %>elements/polyfill.js"></SCRIPT>
+
% if ( $opt{'alt_format'} ) {
<TR>
@@ -324,6 +327,89 @@ Example:
}
</&>
+
+function Location(fieldset) {
+ if ( typeof fieldset == 'String' ) {
+ fieldset = $('#' + fieldset);
+ }
+ this.fieldset = $(fieldset);
+ var errorbox = document.createElement('DIV');
+ errorbox.className = 'error';
+ fieldset.append(errorbox); // after the <table>
+ $(errorbox).position({
+ my: 'left',
+ at: 'right+20px',
+ of: fieldset
+ });
+ this.errorbox = $(errorbox); // so we can find it
+
+ var img_tick = $('<IMG SRC="http://localhost/freeside/images/tick.png">');
+ var img_wait = $('<IMG SRC="http://localhost/freeside/images/wait-orange.gif">');
+
+ // get/set the serialized (URL parameter string) contents of the form fields
+ this.value = function(newvalue) {
+ if (newvalue) {
+ try {
+ this.fieldset.deserialize(newvalue);
+ this.errorbox.empty();
+ if ( newvalue['error'] ) {
+ this.errorbox.text(newvalue['error']);
+ } else {
+ this.errorbox.append(img_tick);
+ }
+ } catch(err) {
+ console.log("Couldn't parse returned data:\n" + newvalue);
+ // show an error also
+ }
+ }
+ return this.fieldset.serialize();
+ };
+
+ // send a standardization request and do something with the result
+ this.standardize = function(callback) {
+ this.errorbox.empty();
+ this.errorbox.append(img_wait);
+ $.ajax({
+ type: 'POST',
+ url: '<% $fsurl %>misc/address_standardize.cgi',
+ success: callback,
+ data: this.value()
+ });
+ };
+
+ // check if required fields are filled, and if so, standardize
+ var standardize_if_ready = function() {
+ var loc = this;
+ var ready = true;
+ var required_fields = this.fieldset.find(':data(required)');
+ for ( var i = 0; ready && i < required_fields.length; i++ ) {
+ if ( required_fields[i].prop('value').length == 0 ) {
+ ready = false;
+ }
+ }
+
+ if ( ready ) {
+ // pass the "value" method, prebound to the location object
+ this.standardize( this.value.bind(loc) );
+ }
+ };
+
+ // event handler; the Location object is passed in event.data
+ var location_change_timer;
+ var location_changed = function( ev ) {
+ if ( location_change_timer ) {
+ window.clearTimeout(location_change_timer);
+ }
+ location_change_timer = window.setTimeout(
+ standardize_if_ready.bind(ev.data),
+ 2000
+ );
+ };
+
+ fieldset.find('input').on('change', this, location_changed);
+ fieldset.find('select').on('change', this, location_changed);
+}
+
</SCRIPT>
<%init>
diff --git a/httemplate/elements/polyfill.js b/httemplate/elements/polyfill.js
new file mode 100644
index 000000000..5e08a9933
--- /dev/null
+++ b/httemplate/elements/polyfill.js
@@ -0,0 +1,30 @@
+// Function.bind(), not supported in IE8
+// polyfill from Mozilla Developer Network
+
+if (!Function.prototype.bind) {
+ Function.prototype.bind = function(oThis) {
+ if (typeof this !== 'function') {
+ // closest thing possible to the ECMAScript 5
+ // internal IsCallable function
+ throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
+ }
+
+ var aArgs = Array.prototype.slice.call(arguments, 1),
+ fToBind = this,
+ fNOP = function() {},
+ fBound = function() {
+ return fToBind.apply(this instanceof fNOP
+ ? this
+ : oThis,
+ aArgs.concat(Array.prototype.slice.call(arguments)));
+ };
+
+ if (this.prototype) {
+ // native functions don't have a prototype
+ fNOP.prototype = this.prototype;
+ }
+ fBound.prototype = new fNOP();
+
+ return fBound;
+ };
+}
diff --git a/httemplate/misc/address_standardize.cgi b/httemplate/misc/address_standardize.cgi
new file mode 100644
index 000000000..d9ba55097
--- /dev/null
+++ b/httemplate/misc/address_standardize.cgi
@@ -0,0 +1,51 @@
+<% encode_json($return) %>\
+<%init>
+
+local $SIG{__DIE__}; #disable Mason error trap
+
+my $DEBUG = 0;
+
+my $conf = new FS::Conf;
+
+# figure out the prefix
+my $pre;
+foreach my $name ($cgi->param) {
+ if ($name =~ /^(\w*)address1$/) {
+ $pre = $1;
+ last;
+ }
+}
+die "no address1 field in location" if !defined($pre);
+
+# gather relevant fields
+my %old = ( map { $_ => scalar($cgi->param($pre . $_)) }
+ qw( company address1 address2 city state zip country )
+);
+
+my $cache = eval { FS::GeocodeCache->standardize(\%old) };
+$cache->set_coord;
+# don't do set_censustract here, though censustract may be set by now
+
+# give the fields their prefixed names back
+# except always name the error string 'error'
+my $error = delete($cache->{'error'}) || '';
+my %new = (
+ 'changed' => 0,
+ 'error' => $error,
+ map { $pre.$_, $cache->get($_) } keys %$cache
+);
+
+foreach ( qw(address1 address2 city state zip country) ) {
+ if ( $new{$pre.$_} ne $old{$pre.$_} ) {
+ $new{changed} = 1;
+ last;
+ }
+}
+
+# refold this to make it acceptable to jquery
+#my $return = [ map { { name => $_, value => $new{$_} } } keys %new ];
+my $return = \%new;
+warn "result:\n".encode_json($return) if $DEBUG;
+
+$r->content_type('application/json');
+</%init>