diff --git a/lib/WebGUI/Shop/Cart.pm b/lib/WebGUI/Shop/Cart.pm index be50659a8..54a07ddc8 100644 --- a/lib/WebGUI/Shop/Cart.pm +++ b/lib/WebGUI/Shop/Cart.pm @@ -848,6 +848,25 @@ sub www_ajaxPrices { #------------------------------------------------------------------- +=head2 www_ajaxSetCartItemShippingId + +Sets the shippingAddressId for a particular cartItem + +=cut + +sub www_ajaxSetCartItemShippingId { + my $self = shift; + my $session = $self->session; + my $form = $session->form; + my $item = $self->getItem($form->get('itemId')); + my $address = $form->get('addressId') || undef; + $item && $item->update({ shippingAddressId => $address }); + $session->http->setMimeType('text/plain'); + return 'ok'; +} + +#------------------------------------------------------------------- + =head2 www_lookupPosUser ( ) Adds a Point of Sale user to the cart. diff --git a/www/extras/shop/cart.js b/www/extras/shop/cart.js index e36e4f3f2..49b8b4fef 100644 --- a/www/extras/shop/cart.js +++ b/www/extras/shop/cart.js @@ -1,157 +1,16 @@ /*global _, window, document, YAHOO */ (function () { - var $event = YAHOO.util.Event, - $connect = YAHOO.util.Connect, - $json = YAHOO.lang.JSON, - prices = null, - addressCache = {}, - elements = { - shipper : 'shipperId_formId', - tax : 'taxWrap', - total : 'totalPriceWrap', - credit : { - available : 'inShopCreditAvailableWrap', - used : 'inShopCreditDeductionWrap' - }, - dropdowns : { - billing : 'billingAddressId_formId', - shipping: 'shippingAddressId_formId' - } - }, - addressParts = [ - 'label', 'firstName', 'lastName', 'organization', 'address1', - 'address2', 'address3', 'city', 'state', 'code', 'country', - 'phoneNumber', 'email' - ]; + function clone(o) { + function F() {} + F.prototype = o; + return new F(); + } function formatCurrency(n) { return parseFloat(n.toString()).toFixed(2); } - function addAddressKind(name) { - var obj = elements[name] = {}; - _.each(addressParts, function (key) { - obj[key] = name + '_' + key + '_formId'; - }); - } - - function getDomElements(o) { - _.each(o, function (v, k) { - if (typeof v === 'object') { - getDomElements(v); - } - else { - o[k] = document.getElementById(v); - } - }); - } - - function sameChange() { - var d = elements.same.checked; - _.each(elements.shipping, function (v, k) { - v.disabled = d; - }); - elements.dropdowns.shipping.disabled = d; - } - - function calculateSummary() { - var shipping = prices.shipping[elements.shipper.value], - shipPrice = (shipping ? - (shipping.hasPrice ? - parseFloat(shipping.price) : - 0) - : 0), - tax = parseFloat(prices.tax), - subtotal = parseFloat(prices.subtotal), - beforeCredit = tax + subtotal + shipPrice, - creditAvail = parseFloat(elements.credit.available.innerHTML), - creditUsed = Math.min(beforeCredit, creditAvail), - afterCredit = beforeCredit - creditUsed; - - elements.credit.used.innerHTML = formatCurrency(creditUsed); - elements.total.innerHTML = formatCurrency(afterCredit); - } - - function updatePrices() { - var selectedShipper = elements.shipper.value, - shipping = elements.dropdowns.shipping.value, - billing = elements.dropdowns.billing.value, - shipper = elements.shipper, - url = window.location.pathname + - '?shop=cart;method=ajaxPrices;' + - ( shipping === 'new_address' ? - '' : 'shippingId=' + shipping) + - ( billing === 'new_address' ? - '' : 'billingId=' + billing); - cb = { - success: function (o) { - var response = $json.parse(o.responseText); - if (response.error) { - return; - } - prices = response; - elements.tax.innerHTML = formatCurrency(response.tax); - _(shipper.options) - .chain() - .map(_.identity) - .each(function (o) { - if (o.value) { - o.parentNode.removeChild(o); - } - }); - _.each(response.shipping, function (o, id) { - var opt = document.createElement('option'), - label = o.label; - if (o.hasPrice) { - label += ' (' + formatCurrency(o.price) + ')'; - } - opt.innerHTML = label; - opt.value = id; - shipper.appendChild(opt); - }); - shipper.value = selectedShipper; - calculateSummary(); - } - }; - $connect.asyncRequest('GET', url, cb); - } - - function updateAddressDropdowns(o) { - var label = o.argument.address.label, - id = o.responseText; - - function updateOne(dropdown) { - var opt = _.detect(dropdown.options, function (o) { - return o.text === label; - }); - - if (!opt) { - opt = document.createElement('option'); - opt.text = label; - dropdown.appendChild(opt); - } - opt.value = id; - } - - updateOne(elements.dropdowns.billing); - updateOne(elements.dropdowns.shipping); - elements.dropdowns[o.argument.name].value = id; - updatePrices(); - } - - function saveAddress(a, name) { - var cb = { - success: updateAddressDropdowns, - argument: { address: a, name: name } - }, - post = 'shop=address;method=ajaxSave;address=' + - $json.stringify(a), - url = window.location.pathname; - - $connect.asyncRequest('POST', url, cb, post); - } - function validAddress(a) { return a.label && a.firstName && @@ -163,108 +22,462 @@ a.country; } - function addressChange(name) { - var other = name === 'billing' ? 'shipping' : 'billing'; - return function () { - var address = {}, - els = elements[name], - label = els.label.value, - oels = elements[other], - copy = oels && oels.label.value === label, - cached = addressCache[label], - dirty; - - if (!cached) { - cached = addressCache[label] = {}; - } - - _.each(elements[name], function (v, k) { - v = v.value; - address[k] = v; - if (cached[k] !== v) { - dirty = true; - cached[k] = v; - } - if (copy) { - oels[k].value = v; - } - }); - if (dirty && validAddress(address)) { - saveAddress(address, name); - } - }; + function addressIdCounts(id) { + return id && + id !== 'new_address' && + id !== 'update_address'; } - function addressUpdater(name) { - var elems = elements[name]; - function update(address) { - _.each(address, function (v, k) { - var dom = elems[k]; - if (dom) { - dom.value = v; + function fillIn(dom, a) { + _.each(a, function (v, k) { + if (dom[k]) { + dom[k].value = v; + } + }); + } + + var Cart = { + attachAddressBlurHandlers: function (name) { + var fields = _.values(this.elements[name]), + handler = this.createAddressBlurHandler(name); + this.event.on(fields, 'focusout', handler); + }, + + attachAddressSelectHandler: function (name) { + var e = this.elements.dropdowns[name], + handler = this.createAddressFiller(name); + + this.event.on(e, 'change', handler); + }, + + attachPerItemShippingChangeHandler: function (select) { + this.event.on(select, 'change', + _.bind(this.setCartItemShippingId, this, select)); + }, + + // Updates the total fields based on information already contained in + // this.prices + calculateSummary: function () { + var e = this.elements, + prices = this.prices, + shipping = prices.shipping[e.shipper.value], + shipPrice = (shipping ? + (shipping.hasPrice ? + parseFloat(shipping.price) : + 0) + : 0), + tax = parseFloat(prices.tax), + subtotal = parseFloat(prices.subtotal), + beforeCredit = tax + subtotal + shipPrice, + creditAvail = parseFloat(e.credit.available.innerHTML), + creditUsed = Math.min(beforeCredit, creditAvail), + afterCredit = beforeCredit - creditUsed; + + e.credit.used.innerHTML = formatCurrency(creditUsed); + e.total.innerHTML = formatCurrency(afterCredit); + }, + + computePerItemShippingOptions: function () { + var self = this, + shipping = this.elements.dropdowns.shipping, + selectedMain = shipping.value, + validOptions = _.select(shipping.options, function (o) { + var v = o.value; + return addressIdCounts(v) && + v !== selectedMain; + }); + + _.each(this.getPerItemShippingDropdowns(), function (d) { + var selected = d.value; + + _(d.options).chain().filter(function (o) { + return o.value; + }).each(_.bind(d.removeChild, d)); + + _.each(validOptions, function (o) { + d.appendChild(o.cloneNode(true)); + }); + + // The idea here is to reselect the option that was selected, + // if it's still valid. If not, we have to tell the backend + // as well. + d.value = selected; + if (d.value !== selected) { + d.value = ''; + self.setCartItemShippingId(d); } }); - updatePrices(); - } - return function () { - var id = this.value, - label = this.options[this.selectedIndex].text, - cached = addressCache[label], - url, cb; + }, - if (cached) { - return update(cached); - } + connect: YAHOO.util.Connect, - url = window.location.pathname + - '?shop=address;method=ajaxGetAddress;addressId=' + - id; + copyBilling: function () { + var self = this, + e = this.elements, + d = e.dropdowns; + d.shipping.value = d.billing.value; + this.getSelectAddress(d.billing, function (address) { + fillIn(e.shipping, address); + self.computePerItemShippingOptions(); + }); + }, - cb = { - success: function (o) { - var address = $json.parse(o.responseText); - addressCache[address.label] = address; - update(address); + create: function (args) { + var self = clone(this); + self.init(args); + }, + + createAddressBlurHandler: function (name) { + var self = this, + other = name === 'billing' ? 'shipping' : 'billing', + e = this.elements[name], + o = this.elements[other], + c = this.addressCache; + + return function () { + var address = {}, + label = e.label.value, + copy = o && o.label.value === label, + cache = c[label], + dirty; + + if (!cache) { + cache = c[label] = {}; + } + + _.each(e, function (v, k) { + v = v.value; + address[k] = v; + if (cache[k] !== v) { + dirty = true; + cache[k] = v; + } + if (copy) { + o[k].value = v; + } + }); + + if (dirty && validAddress(address)) { + self.saveAddress(address, name); } }; - $connect.asyncRequest('GET', url, cb); + }, + + createAddressFiller: function (name) { + var self = this, + e = this.elements[name], + select = this.elements.dropdowns[name]; + + return _.bind(this.getSelectAddress, this, select, function (a) { + fillIn(e, a); + if (name === 'billing' && self.sameShipping()) { + self.copyBilling(); + } + self.updateSummary(); + }); + }, + + dom: YAHOO.util.Dom, + + formatAddress: function (a) { + var et = _.template('<%= email %>'), + csz = _.template('<%= city %>, <%= state %> <%= code %>'); + + return _.compact([ + ' ', + [a.firstName, a.middleName, a.lastName].join(' '), + a.address1, a.address2, a.address3, + csz(a), + a.country, + a.phone, + a.email && et(a) + ]).join('
'); + }, + + getPerItemShippingDropdowns: function () { + return this.dom.getElementsByClassName('itemAddressMenu', 'select'); + }, + + getSelectAddress: function (select, callback) { + var self = this, + id = select.value, + label = select.options[select.selectedIndex].text, + c = this.addressCache, + cache = c[label]; + + if (cache) { + callback(cache); + } + else { + this.request('GET', { + shop : 'address', + method : 'ajaxGetAddress', + addressId : id + }, + function (o) { + var address = self.json.parse(o.responseText); + c[address.label] = address; + callback(address); + }); + } + }, + + event: YAHOO.util.Event, + + init: function (args) { + // this.elements is our cache of dom objects. We're passed in an + // object with ids, and we want to replace those ids with actual + // dom references. + function getElements(o) { + _.each(o, function (v, k) { + if (typeof v === 'object') { + getElements(v); + } + else { + o[k] = document.getElementById(v); + } + }); + } + + var self = this, + e = args.elements, + f = document.forms[0], + checks = f.sameShippingAsBilling, + sameChange; + + getElements(e); + this.elements = e; + + this.prices = null; + this.addressCache = {}; + this.baseUrl = args.baseUrl; + this.attachAddressBlurHandlers('billing'); + this.attachAddressSelectHandler('billing'); + + // if checks is false, we don't have the shipping address form on + // this page (because none of the items in the cart require + // shipping) + if (checks) { + e.same = checks[0]; + sameChange = _.bind(this.useSameShippingAddressChange, this); + this.event.on(checks, 'change', sameChange); + sameChange(); + this.attachAddressBlurHandlers('shipping'); + this.attachAddressSelectHandler('shipping'); + _.each( + this.getPerItemShippingDropdowns(), + _.bind(self.attachPerItemShippingChangeHandler, self) + ); + this.event.on(e.dropdowns.shipping, 'change', function () { + self.computePerItemShippingOptions(); + }); + self.computePerItemShippingOptions(); + } + else { + delete e.shipping; + } + + this.event.on(e.shipper, 'change', + this.calculateSummary, null, this); + + this.updateSummary(); + }, + + json: YAHOO.lang.JSON, + + saveAddress: function (address, name) { + var self = this, + label = address.label; + + this.request('POST', { + shop : 'address', + method : 'ajaxSave', + address : this.json.stringify(address) + }, + function (o) { + var id = o.responseText, + d = self.elements.dropdowns; + + function updateOne(dropdown) { + var opt = _.detect(dropdown.options, function (o) { + return o.text === label; + }); + + if (!opt) { + opt = document.createElement('option'); + opt.text = label; + dropdown.appendChild(opt); + } + + opt.value = id; + } + + updateOne(d.billing); + updateOne(d.shipping); + d[name].value = id; + if (name === 'billing' && self.sameShipping()) { + self.copyBilling(); + } + else { + self.computePerItemShippingOptions(); + } + self.updateSummary(); + }); + }, + + // Like calling calculateSummary, except that it will first fetch + // price information from the server. This should be called when the + // address information has changed, and at least once on page load (so + // we have an initial this.prices to work with) + updateSummary: function () { + var self = this, + e = this.elements, + tax = e.tax, + shipper = e.shipper, + selected = shipper.value, + d = e.dropdowns, + shipping = d.shipping.value, + billing = d.billing.value, + params = { + shop: 'cart', + method: 'ajaxPrices' + }; + + if (addressIdCounts(shipping)) { + params.shippingId = shipping; + } + + if (addressIdCounts(billing)) { + params.billingId = billing; + } + + this.request('GET', params, function (o) { + var response = self.json.parse(o.responseText); + + if (response.error) { + return; + } + + self.prices = response; + tax.innerHTML = formatCurrency(response.tax); + + _(shipper.options).chain().select(function (o) { + return o.value; + }).each(function (o) { + o.parentNode.removeChild(o); + }); + + _.each(response.shipping, function (o, id) { + var opt = document.createElement('option'), + label = o.label; + + if (o.hasPrice) { + label += ' (' + formatCurrency(o.price) + ')'; + } + + opt.innerHTML = label; + opt.value = id; + shipper.appendChild(opt); + }); + + shipper.value = selected; + self.calculateSummary(); + }); + }, + + // This is a very thin layer on top of YAHOO.util.Connect.asyncRequest. + request: function (method, params, success) { + var url = this.baseUrl, + cb = { success: success }, + query = _(params).map(function (v, k) { + return [k, v].join('='); + }).join('&'); + + if (method === 'GET') { + this.connect.asyncRequest(method, url + '?' + query, cb); + } + else { + this.connect.asyncRequest(method, url, cb, query); + } + }, + + sameShipping: function () { + return this.elements.same.checked; + }, + + setCartItemShippingId: function (select) { + var self = this, parent = select.parentNode; + + function setText(t) { + parent.innerHTML = t; + parent.insertBefore(select, parent.firstChild); + } + + this.request('POST', { + shop : 'cart', + method : 'ajaxSetCartItemShippingId', + itemId : select.id.match(/itemAddress_(.*)_formId/)[1], + addressId : select.value + }, function () { + self.updateSummary(); + if (select.value) { + self.getSelectAddress(select, function (address) { + setText(self.formatAddress(address)); + }); + } + else { + setText(''); + } + }); + }, + + useSameShippingAddressChange: function () { + var e = this.elements, + disable = this.sameShipping(), + drops = e.dropdowns; + + _.each(e.shipping, function (v, k) { + v.disabled = disable; + }); + drops.shipping.disabled = disable; + if (disable && addressIdCounts(drops.billing.value)) { + this.copyBilling(); + } + } + }, + addressParts = [ + 'label', 'firstName', 'lastName', 'organization', + 'address1', 'address2', 'address3', 'city', 'state', + 'code', 'country', 'phoneNumber', 'email' + ], + elements = { + shipper : 'shipperId_formId', + tax : 'taxWrap', + total : 'totalPriceWrap', + credit : { + available : 'inShopCreditAvailableWrap', + used : 'inShopCreditDeductionWrap' + }, + dropdowns : { + billing : 'billingAddressId_formId', + shipping: 'shippingAddressId_formId' + } }; + + function addAddressKind(name) { + var obj = elements[name] = {}; + _.each(addressParts, function (key) { + obj[key] = name + '_' + key + '_formId'; + }); } - function handleBlur(name) { - $event.on(_.values(elements[name]), 'focusout', addressChange(name)); - } + addAddressKind('billing'); + addAddressKind('shipping'); - function handleDropdown(name) { - $event.on(elements.dropdowns[name], 'change', addressUpdater(name)); - } - - function main() { - var checks; - addAddressKind('billing'); - addAddressKind('shipping'); - getDomElements(elements); - - elements.form = document.forms[0]; - - handleBlur('billing'); - handleDropdown('billing'); - - checks = elements.form.sameShippingAsBilling; - if (checks) { - elements.same = checks[0]; - $event.on(checks, 'change', sameChange); - sameChange(); - handleBlur('shipping'); - handleDropdown('shipping'); - } - else { - delete elements.shipping; - } - - $event.on(elements.shipper, 'change', calculateSummary); - updatePrices(); - } - - $event.onDOMReady(main); + Cart.event.onDOMReady(function () { + Cart.create({ + baseUrl : window.location.pathname, + elements : elements + }); + }); }());