From 60d1186d0d05df5a6e7bd5747454a78cf3531b72 Mon Sep 17 00:00:00 2001 From: Nikolai R Kristiansen Date: Mon, 13 Jul 2015 23:59:21 +0200 Subject: [PATCH] UI improvements and logic for selected user * Starts work on simple flow for updating member card for existing members --- apps/kassa/templates/kassa/register.html | 21 +- apps/kassa/urls.py | 7 +- apps/kassa/views.py | 46 ++++- static/app/scripts/app.js | 234 ++++++++++++++++++----- static/app/styles/app.scss | 33 ++++ static/app/templates/search_result.html | 67 ++++--- static/dist/scripts/app.js | 234 ++++++++++++++++++----- static/dist/scripts/templates.js | 62 +++--- static/dist/styles/app.css | 2 +- 9 files changed, 552 insertions(+), 154 deletions(-) diff --git a/apps/kassa/templates/kassa/register.html b/apps/kassa/templates/kassa/register.html index eeb2357..66d6b6b 100644 --- a/apps/kassa/templates/kassa/register.html +++ b/apps/kassa/templates/kassa/register.html @@ -7,18 +7,29 @@
-
-
+
+ + {% csrf_token %}

{% trans "Register card" %}

- +
+ +
+ + New user +
+ New card +
+
+
{{ add_card_form|bootstrap }}

Registering a card number with a phone number already tied to an existing user, will add the card to that user.

-
+

{% trans "Search existing users" %}

-
+ {% trans "Firstname, lastname, card number, email" %} + {{ search_user_form|bootstrap_inline }}
diff --git a/apps/kassa/urls.py b/apps/kassa/urls.py index 855cd49..0af7d7e 100644 --- a/apps/kassa/urls.py +++ b/apps/kassa/urls.py @@ -1,9 +1,12 @@ from django.conf.urls import url -from apps.kassa.views import register, inside_user_api, inside_phone_number_api, inside_card_number_api +from apps.kassa.views import register, inside_user_api, inside_phone_number_api, inside_card_number_api, \ + inside_register_api urlpatterns = [ url(r'^$', register, name='register'), url(r'^inside/user/$', inside_user_api, name='inside-user-api'), url(r'^inside/phonenumber/$', inside_phone_number_api, name='inside-phone-number-api'), - url(r'^inside/cardnumber/$', inside_card_number_api, name='inside-card-number-api') + url(r'^inside/cardnumber/$', inside_card_number_api, name='inside-card-number-api'), + url(r'^inside/register/$', inside_register_api, name='inside-register-api') + ] diff --git a/apps/kassa/views.py b/apps/kassa/views.py index 0862eea..e373811 100644 --- a/apps/kassa/views.py +++ b/apps/kassa/views.py @@ -1,12 +1,14 @@ # coding: utf-8 from __future__ import unicode_literals +import json + from django.conf import settings from django.contrib.auth.decorators import login_required from django.http import JsonResponse from django.shortcuts import render +import requests from apps.kassa.forms import AddCardForm, SearchUserForm -import requests @login_required @@ -44,11 +46,45 @@ def inside_phone_number_api(request): @login_required def inside_card_number_api(request): + url = '{}cardnumber.php'.format(settings.INSIDE_API_URL) + params = { + 'apikey': settings.INSIDE_API_KEY + } + + if request.method == 'POST': + post_data = json.loads(request.body) + payload = { + 'card_number': post_data.get('card_number'), + 'user_id': post_data.get('user_id') + } + print payload + response = requests.post( + url, + data=json.dumps(payload), + params=params, + headers=dict(content_type='application/json') + ) + else: + params.update({ + 'card_number': request.GET.get('card_number', '') + }) + response = requests.get(url, params=params).json() + + return JsonResponse(response.json(), status=response.status_code) + + +@login_required +def inside_register_api(request): + post_data = json.loads(request.body) payload = { 'apikey': settings.INSIDE_API_KEY, - 'q': request.GET.get('q', '') + 'phone': post_data.get('phone_number'), + # 'user_id': post_data.get('user_id'), + 'type': post_data.get('type'), # 'renewal' or 'new' + 'cardno': post_data.get('cardno'), + 'source': 'physical' } - url = '{}cardnumber.php'.format(settings.INSIDE_API_URL) - data = requests.get(url, params=payload).json() + url = '{}register.php'.format(settings.INSIDE_API_URL) + response = requests.post(url, data=json.dumps(payload), headers=dict(content_type='application/json')) - return JsonResponse(data) + return JsonResponse(response.json(), status=response.status_code) diff --git a/static/app/scripts/app.js b/static/app/scripts/app.js index e7f7c9e..589201b 100644 --- a/static/app/scripts/app.js +++ b/static/app/scripts/app.js @@ -1,7 +1,50 @@ +function getCookie(name) { + var cookieValue = null; + if (document.cookie && document.cookie !== '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = jQuery.trim(cookies[i]); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) == (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} +function getParameterByName(name) { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), + results = regex.exec(location.search); + return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); +} function format_results(results) { return nunjucks.render('search_results.html', {'results':results}); } +function render_selected_user(user_data) { + user_data.checked = true; + var selected_user_html = nunjucks.render('search_result.html', user_data); + _dom.selectedUserWrap.html(selected_user_html); +} +function set_selected_user(user, update_search_result) { + selectedUser = user; + var new_val = ''; + if(user) { + new_val = user.id; + } + _dom.userIdField.val(new_val).trigger('change'); + if(update_search_result) { + var stale_search_result = _dom.results.find('.search-result').removeClass('selected'); + stale_search_result.find('input').prop('checked', false); + /* User is still in search result, mark selected */ + if( user && _.findWhere(users, {id:user.id}) ) { + var search_result = _dom.results.find('[data-user-id="'+ user.id +'"]').toggleClass('selected'); + search_result.find('input').prop('checked', true); + } + } +} function set_field_state(field_element, state, help_text) { if(!help_text) { @@ -43,7 +86,15 @@ function set_field_state(field_element, state, help_text) { } } -function validate_cardnumber(val) { +function validatePhoneNumber(val) { + if(val.length === 0) { + return 'Phonenumber number should not be empty.'; + } + + return ''; +} + +function validateCardNumber(val) { if(val.length === 0) { return 'Card number should not be empty.'; } @@ -57,20 +108,34 @@ function validate_cardnumber(val) { return ''; } -function validate_form() { +function getFormData(formElement) { + var formData = formElement.serializeArray(); + formData = _.object(_.map(formData, function (x) { + return [x.name, x.value]; + })); + + return formData; +} +function validateCardForm() { + var formData = getFormData(_dom.registerCardForm); var errors = []; /* TODO - phonenumber should be valid (python-phonenumbers) - userid exists (optional) */ - /* Check each fields validation state */ - /* Checkout multifield validation state */ - var cardno_val = _dom.cardNumber.val(); - var cardno_invalid = validate_cardnumber(cardno_val); + /* Card number */ + var cardno_invalid = validateCardNumber(formData.card_number); if(cardno_invalid) { - set_field_state(_dom.cardNumber, 'error', cardno_invalid); + set_field_state(_dom.cardNumberField, 'error', cardno_invalid); errors.push(cardno_invalid); } + /* Phone number */ + var phone_number_invalid = validatePhoneNumber(formData.phone_number); + if(phone_number_invalid) { + set_field_state(_dom.phoneNumberField, 'error', phone_number_invalid); + errors.push(phone_number_invalid); + } + console.log("Errors: ", errors); if(errors.length === 0) { _dom.registerButton.removeAttr('disabled'); @@ -82,22 +147,29 @@ function validate_form() { var _dom; +var register_form_fields = ['user_id', 'phone_number', 'card_number']; +var users; +var selectedUser; $(document).ready(function(){ _dom = { results: $('.results-wrap'), query: $('#id_query'), - phoneNumber: $('#id_phone_number'), - cardNumber: $('#id_card_number'), + phoneNumberLabel: $('[for=id_phone_number]'), + phoneNumberField: $('#id_phone_number'), + cardNumberField: $('#id_card_number'), + registerCardForm: $('.register-card-form'), registerButton: $('#register-submit-btn'), usernameField: $('#id_username'), - userId: $('#id_user_id') + userIdField: $('#id_user_id'), + selectedUserWrap: $('.register-card-form--selected-user-wrap') }; var urls = { insideUserApi: '/inside/user/', insidePhoneNumberApi: '/inside/phonenumber/', - insideCardNumber: '/inside/cardnumber/' + insideCardNumber: '/inside/cardnumber/', + insideRegister: '/inside/register/' }; /* User search as you type */ @@ -110,55 +182,61 @@ $(document).ready(function(){ console.log(data); if(data.results && data.results.length > 0) { _dom.results.html(format_results(data.results)); - } else { + users = data.results; + } + else if(data.error) { + _dom.results.html(data.error); + } + else { if(val === "") { _dom.results.html(''); } else { - _dom.results.html('No existing user with that phone number'); + _dom.results.html("Found no existing user with search param: '"+ val +"'"); } } }); }); /* Phone number as you type */ - _dom.phoneNumber.on('keyup', function() { - var val = _dom.phoneNumber.val().trim(); - if(val.length === 0) { - set_field_state(_dom.phoneNumber, ''); - return 'Phonenumber number should not be empty.'; - } - $.getJSON(urls.insidePhoneNumberApi, {q: val}, function(data) { - if(data.meta && data.meta.num_results == 1) { - set_field_state(_dom.phoneNumber, 'success'); - _dom.userId.val(data.results[0].id); - } else { - set_field_state(_dom.phoneNumber, ''); - _dom.userId.val(''); - } - }); - }); - /* Card number as you type */ - _dom.cardNumber.on('keyup', function() { - if(!validate_form('cardNumber')) { + _dom.phoneNumberField.on('input', function() { + if( !validateCardForm() ) { return; } + var val = _dom.phoneNumberField.val().trim(); + $.getJSON(urls.insidePhoneNumberApi, {q: val}, function(data) { + if(data.meta && data.meta.num_results == 1) { + set_field_state(_dom.phoneNumberField, 'success'); + set_selected_user(data.results[0], true); + } else { + set_field_state(_dom.phoneNumberField, ''); + set_selected_user('', true); + } + }); + + }); + /* Card number as you type */ + _dom.cardNumberField.on('input', function() { /* Card number should exist in the database and not tied to existing user */ - var val = _dom.cardNumber.val().trim(); - $.getJSON(urls.insideCardNumber, {q: val}, function(data) { + if( !validateCardForm() ) { + return; + } + var val = _dom.cardNumberField.val().trim(); + $.getJSON(urls.insideCardNumber, {card_number: val}, function(data) { if(data.error) { - set_field_state(_dom.cardNumber, 'error', data.error); + set_field_state(_dom.cardNumberField, 'error', data.error); return; } if(!data.valid) { - set_field_state(_dom.cardNumber, 'error', 'Cannot find card number in database.'); + set_field_state(_dom.cardNumberField, 'error', 'Cannot find card number in database.'); } else if(data.user !== null && data.valid) { - set_field_state(_dom.cardNumber, 'error', 'Card number belongs to existing user: '+ data.user.firstname +''+ data.user.lastname +'('+data.user.id+').'); + var user = data.user[0]; + set_field_state(_dom.cardNumberField, 'error', 'Card number belongs to existing user: '+ user.firstname +' '+ user.lastname +' ('+user.id+').'); } else if(data.user === null && data.valid) { - set_field_state(_dom.cardNumber, 'success'); + set_field_state(_dom.cardNumberField, 'success'); } else { - set_field_state(_dom.cardNumber, ''); + set_field_state(_dom.cardNumberField, ''); } }); }); @@ -168,23 +246,89 @@ $(document).ready(function(){ var label = $(this).parent(); var number = label.attr('data-phone-number'); var user_id = label.attr('data-user-id'); + + /* Update selected state */ $('.search-result').removeClass('selected'); label.toggleClass('selected'); - _dom.userId.val(user_id); + + set_selected_user(_.findWhere(users, {id: user_id})); + + /* Update phone number field */ if(number === '-') { number = ''; } - _dom.phoneNumber.val(number); + _dom.phoneNumberField.val(number); }); - /* Initial state disabled */ - _dom.registerButton.attr('disabled','disabled'); + _dom.registerButton.on('click', function(e) { + e.preventDefault(); + // TODO on error output validation status + // TODO on success: + // - print what action was performed and suggest steps + // - show new user below form (hide/show after 10s) + // - clear form + + if( !validateCardForm() ) { + // ERROR + return; + } + var url = urls.insideRegister; + var payload = getFormData(_dom.registerCardForm); + + /* Just updating card number? */ + if(payload.user_id && payload.card_number && selectedUser.is_member === "1") { + url = urls.insideCardNumber; + } + $.ajax(url, { + data: JSON.stringify(payload), + contentType: 'application/json', + dataType: 'json', + type: 'post', + headers: {'X-CSRFToken': getCookie('csrftoken')} + }).success(function(data){ + console.log("success", data); + + }).fail(function(data) { + console.log("failed", data); + }); + }); + + /* On user id change */ + _dom.userIdField.on('change', function() { + /* Render selected user in search form */ + console.log("try updating selected user ", selectedUser); + if(selectedUser) { + render_selected_user({res: selectedUser}); + return; + } + render_selected_user({placeholder: true}); + }); + + /* Ninja add some icons to bootstrap form */ + var inputGroup = '
'; + var iconMobile = ''; + _dom.phoneNumberField.wrap(inputGroup); + _dom.phoneNumberField.before(iconMobile); /* Initial focus */ if(_dom.usernameField) { _dom.usernameField.focus(); } - if(_dom.phoneNumber) { - _dom.phoneNumber.focus(); + if(_dom.phoneNumberField) { + _dom.phoneNumberField.focus(); + } + /* Initial submit button state disabled */ + _dom.registerButton.attr('disabled','disabled'); + + /* Load query params */ + /* FIXME: does not trigger validation and selected user preview */ + if( getParameterByName('user_id') ) { + _dom.userIdField.val(getParameterByName('user_id')).trigger('change'); + } + if( getParameterByName('phone_number') ) { + _dom.phoneNumberField.val(getParameterByName('phone_number')); + } + if( getParameterByName('card_number') ) { + _dom.cardNumberField.val(getParameterByName('card_number')); } }); \ No newline at end of file diff --git a/static/app/styles/app.scss b/static/app/styles/app.scss index 907100f..2315275 100644 --- a/static/app/styles/app.scss +++ b/static/app/styles/app.scss @@ -6,6 +6,15 @@ $icon-font-path: '../fonts/'; /* Global styles */ body { } +input[type=number]::-webkit-outer-spin-button, +input[type=number]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type=number] { + -moz-appearance:textfield; +} /* Helpers */ .center-block { @@ -17,6 +26,30 @@ body { text-align: right; margin-top: 10px; } +.register-card-form { + + input[type=tel], + input[type=number] { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; + } + .form-control-feedback { + width: 46px; + height: 46px; + line-height: 46px; + } +} +.glyphicon-phone { + font-size: 26px; +} +.register-card-form--selected-user-wrap { + min-height: 78px; + margin: 10px 0; +} + .query-form { margin-bottom: 20px; } diff --git a/static/app/templates/search_result.html b/static/app/templates/search_result.html index 99c6f81..9a816f2 100644 --- a/static/app/templates/search_result.html +++ b/static/app/templates/search_result.html @@ -1,35 +1,46 @@ -{% if not res.cardno or res.card_is_legacy %} -