diff --git a/tailbone/static/js/jquery.ui.tailbone.mobile.js b/tailbone/static/js/jquery.ui.tailbone.mobile.js new file mode 100644 index 00000000..33a2a7be --- /dev/null +++ b/tailbone/static/js/jquery.ui.tailbone.mobile.js @@ -0,0 +1,78 @@ + +/****************************************** + * jQuery Mobile plugins for Tailbone + *****************************************/ + +/****************************************** + * mobile autocomplete + *****************************************/ + +(function($) { + + $.widget('tailbone.mobileautocomplete', { + + _create: function() { + var that = this; + + // snag some element references + this.search = this.element.find('.ui-input-search'); + this.hidden_field = this.element.find('input[type="hidden"]'); + this.text_field = this.element.find('input[type="text"]'); + this.ul = this.element.find('ul'); + this.button = this.element.find('button'); + + // establish our autocomplete URL + this.url = this.options.url || this.element.data('url'); + + // NOTE: much of this code was copied from the jquery mobile demo site + // https://demos.jquerymobile.com/1.4.5/listview-autocomplete-remote/ + this.ul.on('filterablebeforefilter', function(e, data) { + + var $input = $( data.input ), + value = $input.val(), + html = ""; + that.ul.html( "" ); + if ( value && value.length > 2 ) { + that.ul.html( "
  • " ); + that.ul.listview( "refresh" ); + $.ajax({ + url: that.url, + data: { + term: $input.val() + } + }) + .then( function ( response ) { + $.each( response, function ( i, val ) { + html += '
  • ' + val.label + "
  • "; + }); + that.ul.html( html ); + that.ul.listview( "refresh" ); + that.ul.trigger( "updatelayout"); + }); + } + + }); + + // when user clicks autocomplete result, hide search etc. + this.ul.on('click', 'li', function() { + var $li = $(this); + that.search.hide(); + that.hidden_field.val($li.data('uuid')); + that.button.text($li.text()).show(); + that.ul.hide(); + }); + + // when user clicks "change" button, show search etc. + this.button.click(function() { + that.button.hide(); + that.ul.empty().show(); + that.hidden_field.val(''); + that.search.show(); + that.text_field.focus(); + }); + + } + + }); + +})( jQuery ); diff --git a/tailbone/static/js/tailbone.mobile.js b/tailbone/static/js/tailbone.mobile.js index 9852abba..fb392425 100644 --- a/tailbone/static/js/tailbone.mobile.js +++ b/tailbone/static/js/tailbone.mobile.js @@ -29,13 +29,36 @@ $(document).on('pagecontainerchange', function(event, ui) { }); +$(document).on('pagecreate', function() { + + // setup any autocomplete fields + $('.field.autocomplete').mobileautocomplete(); + +}); + + +/** + * Automatically set focus to certain fields, on various pages + */ +function setfocus() { + var el = null; + var queries = [ + '#username', + '#new-purchasing-batch-vendor-text', + ]; + $.each(queries, function(i, query) { + el = $(query); + if (el.is(':visible')) { + el.focus(); + return false; + } + }); +} + + $(document).on('pageshow', function() { - // on login page, auto-focus username - el = $('#username'); - if (el.is(':visible')) { - el.focus(); - } + setfocus(); // TODO: seems like this should be better somehow... // remove all flash messages after 2.5 seconds @@ -44,8 +67,28 @@ $(document).on('pageshow', function() { }); -$(document).on('click', '#datasync-restart', function() { +// vendor validation for new purchasing batch +$(document).on('click', 'form[name="new-purchasing-batch"] input[type="submit"]', function() { + var $form = $(this).parents('form'); + if (! $form.find('[name="vendor"]').val()) { + alert("Please select a vendor"); + $form.find('[name="new-purchasing-batch-vendor-text"]').focus(); + return false; + } +}); - // disable datasync restart button when clicked +// submit new purchasing batch form on Purchase click +$(document).on('click', 'form[name="new-purchasing-batch"] [data-role="listview"] a', function() { + var $form = $(this).parents('form'); + var $field = $form.find('[name="purchase"]'); + var uuid = $(this).parents('li').data('uuid'); + $field.val(uuid); + $form.submit(); + return false; +}); + + +// disable datasync restart button when clicked +$(document).on('click', '#datasync-restart', function() { $(this).button('disable'); }); diff --git a/tailbone/templates/mobile/base.mako b/tailbone/templates/mobile/base.mako index 31051cd5..575ce278 100644 --- a/tailbone/templates/mobile/base.mako +++ b/tailbone/templates/mobile/base.mako @@ -5,16 +5,22 @@ ${self.global_title()} » ${self.title()} + ${h.javascript_link('https://code.jquery.com/jquery-1.12.4.min.js')} ${h.javascript_link('https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js')} - ${h.stylesheet_link('https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css')} + ${h.javascript_link(request.static_url('tailbone:static/js/jquery.ui.tailbone.mobile.js'))} ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.mobile.js'))} + ${self.extra_javascript()} + + ${h.stylesheet_link('https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css')} ${h.stylesheet_link(request.static_url('tailbone:static/css/mobile.css'))} % if not request.rattail_config.production(): - + % endif + ${self.extra_styles()} + ${self.mobile_body()} @@ -47,6 +53,10 @@ <%def name="page_title()">${self.title()} +<%def name="extra_javascript()"> + +<%def name="extra_styles()"> + <%def name="mobile_header()">
    ${self.mobile_header_link()} diff --git a/tailbone/templates/purchases/batches/mobile_create.mako b/tailbone/templates/purchases/batches/mobile_create.mako new file mode 100644 index 00000000..c2c3c55a --- /dev/null +++ b/tailbone/templates/purchases/batches/mobile_create.mako @@ -0,0 +1,49 @@ +## -*- coding: utf-8 -*- +<%inherit file="/mobile/base.mako" /> + +<%def name="title()">New ${mode_title} Batch + +${h.form(request.current_route_url(), class_='ui-filterable', name='new-purchasing-batch')} +${h.csrf_token(request)} + +% if vendor is Undefined: + +
    +
    + ${h.hidden('vendor')} + ${h.text('new-purchasing-batch-vendor-text', placeholder="Vendor name", autocomplete='off', **{'data-type': 'search'})} +
      + +
      +
      + +
      + ${h.submit('submit', "Find purchase orders")} + ## + +% else: ## vendor is known + +
      +
      + ${h.hidden('vendor', value=vendor.uuid)} + ${vendor} +
      +
      + + % if purchases: + ${h.hidden('purchase')} + + % else: +

      (no eligible purchases found)

      + % endif + + ## ${h.link_to("Receive from scratch for {}".format(vendor), '#', class_='ui-btn ui-corner-all')} + ${h.link_to("Start over", url('purchases.batch.mobile_create'), class_='ui-btn ui-corner-all')} + +% endif + +${h.end_form()} diff --git a/tailbone/views/purchases/batch.py b/tailbone/views/purchases/batch.py index 0f1599e5..9b73a6f6 100644 --- a/tailbone/views/purchases/batch.py +++ b/tailbone/views/purchases/batch.py @@ -240,14 +240,16 @@ class PurchaseBatchView(BatchMasterView): fs.department.set(readonly=True) fs.purchase.set(readonly=True) - def eligible_purchases(self): - uuid = self.request.GET.get('vendor_uuid') - vendor = Session.query(model.Vendor).get(uuid) if uuid else None + def eligible_purchases(self, vendor_uuid=None, mode=None): + if not vendor_uuid: + vendor_uuid = self.request.GET.get('vendor_uuid') + vendor = Session.query(model.Vendor).get(vendor_uuid) if vendor_uuid else None if not vendor: return {'error': "Must specify a vendor."} - mode = self.request.GET.get('mode') - mode = int(mode) if mode and mode.isdigit() else None + if mode is None: + mode = self.request.GET.get('mode') + mode = int(mode) if mode and mode.isdigit() else None if not mode or mode not in self.enum.PURCHASE_BATCH_MODE: return {'error': "Unknown mode: {}".format(mode)} @@ -888,6 +890,37 @@ class PurchaseBatchView(BatchMasterView): self.mobile = True return self.render_to_response('mobile_index', {}) + def mobile_create(self): + """ + View for creating a new purchasing batch via mobile + """ + # TODO: make this dynamic somehow, support other modes + mode = self.enum.PURCHASE_BATCH_MODE_RECEIVING + data = {'mode': mode} + + vendor = None + if self.request.method == 'POST' and self.request.POST.get('vendor'): + vendor = self.Session.query(model.Vendor).get(self.request.POST['vendor']) + if vendor: + data['vendor'] = vendor + if self.request.POST.get('purchase'): + purchase = self.get_purchase(self.request.POST['purchase']) + if purchase: + # TODO: these kwargs need help! + batch = self.handler.make_batch(self.Session(), + mode=mode, vendor=vendor, + store=self.rattail_config.get_store(self.Session()), + buyer=self.request.user.employee, + created_by=self.request.user) + self.request.session.flash("Created new purchasing batch: {}".format(batch)) + return self.redirect(self.request.route_url('purchases.batch.mobile_create')) + + data['mode_title'] = self.enum.PURCHASE_BATCH_MODE[mode].capitalize() + if vendor: + purchases = self.eligible_purchases(vendor.uuid, mode=mode) + data['purchases'] = [(p['key'], p['display']) for p in purchases['purchases']] + return self.render_to_response('mobile_create', data) + @classmethod def defaults(cls, config): route_prefix = cls.get_route_prefix() @@ -896,11 +929,16 @@ class PurchaseBatchView(BatchMasterView): model_key = cls.get_model_key() model_title = cls.get_model_title() - # mobile + # mobile index config.add_route('{}.mobile'.format(route_prefix), '/mobile{}'.format(url_prefix)) config.add_view(cls, attr='mobile_index', route_name='{}.mobile'.format(route_prefix), permission='{}.list'.format(permission_prefix)) + # mobile create + config.add_route('{}.mobile_create'.format(route_prefix), '/mobile{}/new'.format(url_prefix)) + config.add_view(cls, attr='mobile_create', route_name='{}.mobile_create'.format(route_prefix), + permission='{}.create'.format(permission_prefix)) + # eligible purchases (AJAX) config.add_route('{}.eligible_purchases'.format(route_prefix), '{}/eligible-purchases'.format(url_prefix)) config.add_view(cls, attr='eligible_purchases', route_name='{}.eligible_purchases'.format(route_prefix),